mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-10 12:38:52 +01:00
📝 feat: Add Markdown Rendering Support for Artifacts (#10049)
* Add Markdown rendering support for artifacts * Add tests * Remove custom code for mermaid * Remove unnecessary dark mode hook * refactor: optimize mermaid dependencies - Added support for additional MIME types in artifact templates. - Updated mermaidDependencies to include new packages: class-variance-authority, clsx, tailwind-merge, and @radix-ui/react-slot. - Refactored zoom and refresh icons in MermaidDiagram component for improved clarity and maintainability. * fix: add Markdown support for artifacts rendering * feat: support 'text/md' as an additional MIME type for Markdown artifacts * refactor: simplify markdownDependencies structure in artifacts utility --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
e9a85d5c65
commit
7c9a868d34
13 changed files with 760 additions and 88 deletions
185
client/src/utils/__tests__/markdown.test.ts
Normal file
185
client/src/utils/__tests__/markdown.test.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { getMarkdownFiles } from '../markdown';
|
||||
|
||||
describe('markdown artifacts', () => {
|
||||
describe('getMarkdownFiles', () => {
|
||||
it('should return content.md with the original markdown content', () => {
|
||||
const markdown = '# Hello World\n\nThis is a test.';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
});
|
||||
|
||||
it('should return default content when markdown is empty', () => {
|
||||
const files = getMarkdownFiles('');
|
||||
|
||||
expect(files['content.md']).toBe('# No content provided');
|
||||
});
|
||||
|
||||
it('should include App.tsx with MarkdownRenderer component', () => {
|
||||
const markdown = '# Test';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['App.tsx']).toContain('import React from');
|
||||
expect(files['App.tsx']).toContain(
|
||||
"import MarkdownRenderer from '/components/ui/MarkdownRenderer'",
|
||||
);
|
||||
expect(files['App.tsx']).toContain('<MarkdownRenderer content={');
|
||||
expect(files['App.tsx']).toContain('export default App');
|
||||
});
|
||||
|
||||
it('should include index.tsx entry point', () => {
|
||||
const markdown = '# Test';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['index.tsx']).toContain('import App from "./App"');
|
||||
expect(files['index.tsx']).toContain('import "./styles.css"');
|
||||
expect(files['index.tsx']).toContain('import "./markdown.css"');
|
||||
expect(files['index.tsx']).toContain('createRoot');
|
||||
});
|
||||
|
||||
it('should include MarkdownRenderer component file', () => {
|
||||
const markdown = '# Test';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('import Markdown from');
|
||||
expect(files['/components/ui/MarkdownRenderer.tsx']).toContain('MarkdownRendererProps');
|
||||
expect(files['/components/ui/MarkdownRenderer.tsx']).toContain(
|
||||
'export default MarkdownRenderer',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include markdown.css with styling', () => {
|
||||
const markdown = '# Test';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['markdown.css']).toContain('.markdown-body');
|
||||
expect(files['markdown.css']).toContain('list-style-type: disc');
|
||||
expect(files['markdown.css']).toContain('prefers-color-scheme: dark');
|
||||
});
|
||||
|
||||
describe('content escaping', () => {
|
||||
it('should escape backticks in markdown content', () => {
|
||||
const markdown = 'Here is some `inline code`';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['App.tsx']).toContain('\\`');
|
||||
});
|
||||
|
||||
it('should escape backslashes in markdown content', () => {
|
||||
const markdown = 'Path: C:\\Users\\Test';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['App.tsx']).toContain('\\\\');
|
||||
});
|
||||
|
||||
it('should escape dollar signs in markdown content', () => {
|
||||
const markdown = 'Price: $100';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['App.tsx']).toContain('\\$');
|
||||
});
|
||||
|
||||
it('should handle code blocks with backticks', () => {
|
||||
const markdown = '```js\nconsole.log("test");\n```';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
// Should be escaped
|
||||
expect(files['App.tsx']).toContain('\\`\\`\\`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list indentation normalization', () => {
|
||||
it('should normalize 2-space indented lists to 4-space', () => {
|
||||
const markdown = '- Item 1\n - Subitem 1\n - Subitem 2';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
// The indentation normalization happens in wrapMarkdownRenderer
|
||||
// It converts 2 spaces before list markers to 4 spaces
|
||||
// Check that content.md preserves the original, but App.tsx has normalized content
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
expect(files['App.tsx']).toContain('- Item 1');
|
||||
expect(files['App.tsx']).toContain('Subitem 1');
|
||||
});
|
||||
|
||||
it('should handle numbered lists with 2-space indents', () => {
|
||||
const markdown = '1. First\n 2. Second nested';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
// Verify normalization occurred
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
expect(files['App.tsx']).toContain('1. First');
|
||||
expect(files['App.tsx']).toContain('2. Second nested');
|
||||
});
|
||||
|
||||
it('should not affect already 4-space indented lists', () => {
|
||||
const markdown = '- Item 1\n - Subitem 1';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
// Already normalized, should be preserved
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
expect(files['App.tsx']).toContain('- Item 1');
|
||||
expect(files['App.tsx']).toContain('Subitem 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle very long markdown content', () => {
|
||||
const longMarkdown = '# Test\n\n' + 'Lorem ipsum '.repeat(1000);
|
||||
const files = getMarkdownFiles(longMarkdown);
|
||||
|
||||
expect(files['content.md']).toBe(longMarkdown);
|
||||
expect(files['App.tsx']).toContain('Lorem ipsum');
|
||||
});
|
||||
|
||||
it('should handle markdown with special characters', () => {
|
||||
const markdown = '# Test & < > " \'';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
});
|
||||
|
||||
it('should handle markdown with unicode characters', () => {
|
||||
const markdown = '# 你好 世界 🌍';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
});
|
||||
|
||||
it('should handle markdown with only whitespace', () => {
|
||||
const markdown = ' \n\n ';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
});
|
||||
|
||||
it('should handle markdown with mixed line endings', () => {
|
||||
const markdown = '# Line 1\r\n## Line 2\n### Line 3';
|
||||
const files = getMarkdownFiles(markdown);
|
||||
|
||||
expect(files['content.md']).toBe(markdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown component structure', () => {
|
||||
it('should generate a MarkdownRenderer component that uses marked-react', () => {
|
||||
const files = getMarkdownFiles('# Test');
|
||||
const rendererCode = files['/components/ui/MarkdownRenderer.tsx'];
|
||||
|
||||
// Verify the component imports and uses Markdown from marked-react
|
||||
expect(rendererCode).toContain("import Markdown from 'marked-react'");
|
||||
expect(rendererCode).toContain('<Markdown gfm={true} breaks={true}>{content}</Markdown>');
|
||||
});
|
||||
|
||||
it('should pass markdown content to the Markdown component', () => {
|
||||
const testContent = '# Heading\n- List item';
|
||||
const files = getMarkdownFiles(testContent);
|
||||
const appCode = files['App.tsx'];
|
||||
|
||||
// The App.tsx should pass the content to MarkdownRenderer
|
||||
expect(appCode).toContain('<MarkdownRenderer content={');
|
||||
expect(appCode).toContain('# Heading');
|
||||
expect(appCode).toContain('- List item');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,10 +6,10 @@ import type {
|
|||
} from '@codesandbox/sandpack-react';
|
||||
|
||||
const artifactFilename = {
|
||||
'application/vnd.mermaid': 'App.tsx',
|
||||
'application/vnd.react': 'App.tsx',
|
||||
'text/html': 'index.html',
|
||||
'application/vnd.code-html': 'index.html',
|
||||
// mermaid and markdown types are handled separately in useArtifactProps.ts
|
||||
default: 'index.html',
|
||||
// 'css': 'css',
|
||||
// 'javascript': 'js',
|
||||
|
|
@ -19,13 +19,20 @@ const artifactFilename = {
|
|||
};
|
||||
|
||||
const artifactTemplate: Record<
|
||||
keyof typeof artifactFilename,
|
||||
| keyof typeof artifactFilename
|
||||
| 'application/vnd.mermaid'
|
||||
| 'text/markdown'
|
||||
| 'text/md'
|
||||
| 'text/plain',
|
||||
SandpackPredefinedTemplate | undefined
|
||||
> = {
|
||||
'text/html': 'static',
|
||||
'application/vnd.react': 'react-ts',
|
||||
'application/vnd.mermaid': 'react-ts',
|
||||
'application/vnd.code-html': 'static',
|
||||
'text/markdown': 'react-ts',
|
||||
'text/md': 'react-ts',
|
||||
'text/plain': 'react-ts',
|
||||
default: 'static',
|
||||
// 'css': 'css',
|
||||
// 'javascript': 'js',
|
||||
|
|
@ -34,27 +41,6 @@ const artifactTemplate: Record<
|
|||
// 'tsx': 'tsx',
|
||||
};
|
||||
|
||||
export function getFileExtension(language?: string): string {
|
||||
switch (language) {
|
||||
case 'application/vnd.react':
|
||||
return 'tsx';
|
||||
case 'application/vnd.mermaid':
|
||||
return 'mermaid';
|
||||
case 'text/html':
|
||||
return 'html';
|
||||
// case 'jsx':
|
||||
// return 'jsx';
|
||||
// case 'tsx':
|
||||
// return 'tsx';
|
||||
// case 'html':
|
||||
// return 'html';
|
||||
// case 'css':
|
||||
// return 'css';
|
||||
default:
|
||||
return 'txt';
|
||||
}
|
||||
}
|
||||
|
||||
export function getKey(type: string, language?: string): string {
|
||||
return `${type}${(language?.length ?? 0) > 0 ? `-${language}` : ''}`;
|
||||
}
|
||||
|
|
@ -109,19 +95,34 @@ const standardDependencies = {
|
|||
vaul: '^0.9.1',
|
||||
};
|
||||
|
||||
const mermaidDependencies = Object.assign(
|
||||
{
|
||||
mermaid: '^11.4.1',
|
||||
'react-zoom-pan-pinch': '^3.6.1',
|
||||
},
|
||||
standardDependencies,
|
||||
);
|
||||
const mermaidDependencies = {
|
||||
mermaid: '^11.4.1',
|
||||
'react-zoom-pan-pinch': '^3.6.1',
|
||||
'class-variance-authority': '^0.6.0',
|
||||
clsx: '^1.2.1',
|
||||
'tailwind-merge': '^1.9.1',
|
||||
'@radix-ui/react-slot': '^1.1.0',
|
||||
};
|
||||
|
||||
const dependenciesMap: Record<keyof typeof artifactFilename, object> = {
|
||||
const markdownDependencies = {
|
||||
'marked-react': '^2.0.0',
|
||||
};
|
||||
|
||||
const dependenciesMap: Record<
|
||||
| keyof typeof artifactFilename
|
||||
| 'application/vnd.mermaid'
|
||||
| 'text/markdown'
|
||||
| 'text/md'
|
||||
| 'text/plain',
|
||||
Record<string, string>
|
||||
> = {
|
||||
'application/vnd.mermaid': mermaidDependencies,
|
||||
'application/vnd.react': standardDependencies,
|
||||
'text/html': standardDependencies,
|
||||
'application/vnd.code-html': standardDependencies,
|
||||
'text/markdown': markdownDependencies,
|
||||
'text/md': markdownDependencies,
|
||||
'text/plain': markdownDependencies,
|
||||
default: standardDependencies,
|
||||
};
|
||||
|
||||
|
|
|
|||
256
client/src/utils/markdown.ts
Normal file
256
client/src/utils/markdown.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import dedent from 'dedent';
|
||||
|
||||
const markdownRenderer = dedent(`import React, { useEffect, useState } from 'react';
|
||||
import Markdown from 'marked-react';
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => {
|
||||
return (
|
||||
<div
|
||||
className="markdown-body"
|
||||
style={{
|
||||
padding: '2rem',
|
||||
margin: '1rem',
|
||||
minHeight: '100vh'
|
||||
}}
|
||||
>
|
||||
<Markdown gfm={true} breaks={true}>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRenderer;`);
|
||||
|
||||
const wrapMarkdownRenderer = (content: string) => {
|
||||
// Normalize indentation: convert 2-space indents to 4-space for proper nesting
|
||||
const normalizedContent = content.replace(/^( {2})(-|\d+\.)/gm, ' $2');
|
||||
|
||||
// Escape backticks, backslashes, and dollar signs in the content
|
||||
const escapedContent = normalizedContent
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
|
||||
return dedent(`import React from 'react';
|
||||
import MarkdownRenderer from '/components/ui/MarkdownRenderer';
|
||||
|
||||
const App = () => {
|
||||
return <MarkdownRenderer content={\`${escapedContent}\`} />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
`);
|
||||
};
|
||||
|
||||
const markdownCSS = `
|
||||
/* GitHub Markdown CSS - Light theme base */
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
line-height: 1.5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
color: #24292f;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2 {
|
||||
border-bottom: 1px solid #d0d7de;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown-body h1 { font-size: 2em; margin: 0.67em 0; }
|
||||
.markdown-body h2 { font-size: 1.5em; }
|
||||
.markdown-body h3 { font-size: 1.25em; }
|
||||
.markdown-body h4 { font-size: 1em; }
|
||||
.markdown-body h5 { font-size: 0.875em; }
|
||||
.markdown-body h6 { font-size: 0.85em; }
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
list-style: revert !important;
|
||||
padding-left: 2em !important;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body ul { list-style-type: disc !important; }
|
||||
.markdown-body ol { list-style-type: decimal !important; }
|
||||
.markdown-body ul ul { list-style-type: circle !important; }
|
||||
.markdown-body ul ul ul { list-style-type: square !important; }
|
||||
|
||||
.markdown-body li { margin-top: 0.25em; }
|
||||
|
||||
.markdown-body li:has(> input[type="checkbox"]) {
|
||||
list-style-type: none !important;
|
||||
}
|
||||
|
||||
.markdown-body li > input[type="checkbox"] {
|
||||
margin-right: 0.75em;
|
||||
margin-left: -1.5em;
|
||||
vertical-align: middle;
|
||||
pointer-events: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item {
|
||||
list-style-type: none !important;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item > input[type="checkbox"] {
|
||||
margin-right: 0.75em;
|
||||
margin-left: -1.5em;
|
||||
vertical-align: middle;
|
||||
pointer-events: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
color: #24292f;
|
||||
font-family: ui-monospace, monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
border-radius: 6px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
text-decoration: none;
|
||||
color: #0969da;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-body table thead {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-body table th, .markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
border-left: 0.25em solid #d0d7de;
|
||||
margin: 0 0 16px 0;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
border: 0;
|
||||
background-color: #d0d7de;
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.markdown-body {
|
||||
color: #c9d1d9;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2 {
|
||||
border-bottom-color: #21262d;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: rgba(110, 118, 129, 0.4);
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #161b22;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
.markdown-body table thead {
|
||||
background-color: #161b22;
|
||||
}
|
||||
|
||||
.markdown-body table th, .markdown-body table td {
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
border-left-color: #3b434b;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
background-color: #21262d;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getMarkdownFiles = (content: string) => {
|
||||
return {
|
||||
'content.md': content || '# No content provided',
|
||||
'App.tsx': wrapMarkdownRenderer(content),
|
||||
'index.tsx': dedent(`import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./styles.css";
|
||||
import "./markdown.css";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const root = createRoot(document.getElementById("root"));
|
||||
root.render(<App />);
|
||||
;`),
|
||||
'/components/ui/MarkdownRenderer.tsx': markdownRenderer,
|
||||
'markdown.css': markdownCSS,
|
||||
};
|
||||
};
|
||||
|
|
@ -7,9 +7,34 @@ import {
|
|||
ReactZoomPanPinchRef,
|
||||
} from "react-zoom-pan-pinch";
|
||||
import mermaid from "mermaid";
|
||||
import { ZoomIn, ZoomOut, RefreshCw } from "lucide-react";
|
||||
import { Button } from "/components/ui/button";
|
||||
|
||||
const ZoomIn = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" x2="16.65" y1="21" y2="16.65"/>
|
||||
<line x1="11" x2="11" y1="8" y2="14"/>
|
||||
<line x1="8" x2="14" y1="11" y2="11"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ZoomOut = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" x2="16.65" y1="21" y2="16.65"/>
|
||||
<line x1="8" x2="14" y1="11" y2="11"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const RefreshCw = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||
<path d="M21 3v5h-5"/>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||
<path d="M8 16H3v5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface MermaidDiagramProps {
|
||||
content: string;
|
||||
}
|
||||
|
|
@ -181,21 +206,21 @@ const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
|
|||
</TransformComponent>
|
||||
<div className="absolute bottom-2 right-2 flex space-x-2">
|
||||
<Button onClick={() => zoomIn(0.1)} variant="outline" size="icon">
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
<ZoomIn />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => zoomOut(0.1)}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
<ZoomOut />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={centerAndFitDiagram}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<RefreshCw />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -217,12 +242,20 @@ export default App = () => (
|
|||
`);
|
||||
};
|
||||
|
||||
const mermaidCSS = `
|
||||
body {
|
||||
background-color: #282C34;
|
||||
}
|
||||
`;
|
||||
|
||||
export const getMermaidFiles = (content: string) => {
|
||||
return {
|
||||
'diagram.mmd': content || '# No mermaid diagram content provided',
|
||||
'App.tsx': wrapMermaidDiagram(content),
|
||||
'index.tsx': dedent(`import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./styles.css";
|
||||
import "./mermaid.css";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
|
|
@ -230,5 +263,6 @@ const root = createRoot(document.getElementById("root"));
|
|||
root.render(<App />);
|
||||
;`),
|
||||
'/components/ui/MermaidDiagram.tsx': mermaid,
|
||||
'mermaid.css': mermaidCSS,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue