diff --git a/api/app/clients/prompts/artifacts.js b/api/app/clients/prompts/artifacts.js index b907a16b56..915ccae629 100644 --- a/api/app/clients/prompts/artifacts.js +++ b/api/app/clients/prompts/artifacts.js @@ -3,6 +3,7 @@ const { EModelEndpoint, ArtifactModes } = require('librechat-data-provider'); const { generateShadcnPrompt } = require('~/app/clients/prompts/shadcn-docs/generate'); const { components } = require('~/app/clients/prompts/shadcn-docs/components'); +/** @deprecated */ // eslint-disable-next-line no-unused-vars const artifactsPromptV1 = dedent`The assistant can create and reference artifacts during conversations. @@ -115,6 +116,7 @@ Here are some examples of correct usage of artifacts: `; + const artifactsPrompt = dedent`The assistant can create and reference artifacts during conversations. Artifacts are for substantial, self-contained content that users might modify or reuse, displayed in a separate UI window for clarity. @@ -165,6 +167,10 @@ Artifacts are for substantial, self-contained content that users might modify or - SVG: "image/svg+xml" - The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags. - The assistant should specify the viewbox of the SVG rather than defining a width/height + - Markdown: "text/markdown" or "text/md" + - The user interface will render Markdown content placed within the artifact tags. + - Supports standard Markdown syntax including headers, lists, links, images, code blocks, tables, and more. + - Both "text/markdown" and "text/md" are accepted as valid MIME types for Markdown content. - Mermaid Diagrams: "application/vnd.mermaid" - The user interface will render Mermaid diagrams placed within the artifact tags. - React Components: "application/vnd.react" @@ -366,6 +372,10 @@ Artifacts are for substantial, self-contained content that users might modify or - SVG: "image/svg+xml" - The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags. - The assistant should specify the viewbox of the SVG rather than defining a width/height + - Markdown: "text/markdown" or "text/md" + - The user interface will render Markdown content placed within the artifact tags. + - Supports standard Markdown syntax including headers, lists, links, images, code blocks, tables, and more. + - Both "text/markdown" and "text/md" are accepted as valid MIME types for Markdown content. - Mermaid Diagrams: "application/vnd.mermaid" - The user interface will render Mermaid diagrams placed within the artifact tags. - React Components: "application/vnd.react" diff --git a/client/src/components/Artifacts/ArtifactCodeEditor.tsx b/client/src/components/Artifacts/ArtifactCodeEditor.tsx index 62f2dc4e85..f4f25280f9 100644 --- a/client/src/components/Artifacts/ArtifactCodeEditor.tsx +++ b/client/src/components/Artifacts/ArtifactCodeEditor.tsx @@ -144,9 +144,10 @@ export const ArtifactCodeEditor = function ({ } return { ...sharedOptions, + activeFile: '/' + fileKey, bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL, }; - }, [config, template]); + }, [config, template, fileKey]); const [readOnly, setReadOnly] = useState(isSubmitting ?? false); useEffect(() => { setReadOnly(isSubmitting ?? false); diff --git a/client/src/components/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx index d5114ceafc..3764119f3a 100644 --- a/client/src/components/Artifacts/ArtifactPreview.tsx +++ b/client/src/components/Artifacts/ArtifactPreview.tsx @@ -13,7 +13,6 @@ export const ArtifactPreview = memo(function ({ files, fileKey, template, - isMermaid, sharedProps, previewRef, currentCode, @@ -21,7 +20,6 @@ export const ArtifactPreview = memo(function ({ }: { files: ArtifactFiles; fileKey: string; - isMermaid: boolean; template: SandpackProviderProps['template']; sharedProps: Partial; previewRef: React.MutableRefObject; @@ -56,15 +54,6 @@ export const ArtifactPreview = memo(function ({ return _options; }, [startupConfig, template]); - const style: PreviewProps['style'] | undefined = useMemo(() => { - if (isMermaid) { - return { - backgroundColor: '#282C34', - }; - } - return; - }, [isMermaid]); - if (Object.keys(artifactFiles).length === 0) { return null; } @@ -84,7 +73,6 @@ export const ArtifactPreview = memo(function ({ showRefreshButton={false} tabIndex={0} ref={previewRef} - style={style} /> ); diff --git a/client/src/components/Artifacts/ArtifactTabs.tsx b/client/src/components/Artifacts/ArtifactTabs.tsx index a463aca792..8a5b14d556 100644 --- a/client/src/components/Artifacts/ArtifactTabs.tsx +++ b/client/src/components/Artifacts/ArtifactTabs.tsx @@ -8,17 +8,14 @@ import { useAutoScroll } from '~/hooks/Artifacts/useAutoScroll'; import { ArtifactCodeEditor } from './ArtifactCodeEditor'; import { useGetStartupConfig } from '~/data-provider'; import { ArtifactPreview } from './ArtifactPreview'; -import { MermaidMarkdown } from './MermaidMarkdown'; import { cn } from '~/utils'; export default function ArtifactTabs({ artifact, - isMermaid, editorRef, previewRef, }: { artifact: Artifact; - isMermaid: boolean; editorRef: React.MutableRefObject; previewRef: React.MutableRefObject; }) { @@ -46,25 +43,20 @@ export default function ArtifactTabs({ className={cn('flex-grow overflow-auto')} tabIndex={-1} > - {isMermaid ? ( - - ) : ( - - )} + {/* Content */} } previewRef={previewRef as React.MutableRefObject} diff --git a/client/src/components/Artifacts/MermaidMarkdown.tsx b/client/src/components/Artifacts/MermaidMarkdown.tsx deleted file mode 100644 index 780b0d74da..0000000000 --- a/client/src/components/Artifacts/MermaidMarkdown.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { CodeMarkdown } from './Code'; - -export function MermaidMarkdown({ - content, - isSubmitting, -}: { - content: string; - isSubmitting: boolean; -}) { - return ; -} diff --git a/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts new file mode 100644 index 0000000000..f9f29e0c56 --- /dev/null +++ b/client/src/hooks/Artifacts/__tests__/useArtifactProps.test.ts @@ -0,0 +1,219 @@ +import { renderHook } from '@testing-library/react'; +import useArtifactProps from '../useArtifactProps'; +import type { Artifact } from '~/common'; + +describe('useArtifactProps', () => { + const createArtifact = (partial: Partial): Artifact => ({ + id: 'test-id', + lastUpdateTime: Date.now(), + ...partial, + }); + + describe('markdown artifacts', () => { + it('should handle text/markdown type with content.md as fileKey', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '# Hello World\n\nThis is markdown.', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.fileKey).toBe('content.md'); + expect(result.current.template).toBe('react-ts'); + }); + + it('should handle text/plain type with content.md as fileKey', () => { + const artifact = createArtifact({ + type: 'text/plain', + content: '# Plain text as markdown', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.fileKey).toBe('content.md'); + expect(result.current.template).toBe('react-ts'); + }); + + it('should include content.md in files with original markdown', () => { + const markdownContent = '# Test\n\n- Item 1\n- Item 2'; + const artifact = createArtifact({ + type: 'text/markdown', + content: markdownContent, + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.files['content.md']).toBe(markdownContent); + }); + + it('should include App.tsx with wrapped markdown renderer', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '# Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.files['App.tsx']).toContain('MarkdownRenderer'); + expect(result.current.files['App.tsx']).toContain('import React from'); + }); + + it('should include all required markdown files', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '# Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + // Check all required files are present + expect(result.current.files['content.md']).toBeDefined(); + expect(result.current.files['App.tsx']).toBeDefined(); + expect(result.current.files['index.tsx']).toBeDefined(); + expect(result.current.files['/components/ui/MarkdownRenderer.tsx']).toBeDefined(); + expect(result.current.files['markdown.css']).toBeDefined(); + }); + + it('should escape special characters in markdown content', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: 'Code: `const x = 1;`\nPath: C:\\Users', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + // Original content should be preserved in content.md + expect(result.current.files['content.md']).toContain('`const x = 1;`'); + expect(result.current.files['content.md']).toContain('C:\\Users'); + + // App.tsx should have escaped content + expect(result.current.files['App.tsx']).toContain('\\`'); + expect(result.current.files['App.tsx']).toContain('\\\\'); + }); + + it('should handle empty markdown content', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.files['content.md']).toBe('# No content provided'); + }); + + it('should handle undefined markdown content', () => { + const artifact = createArtifact({ + type: 'text/markdown', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.files['content.md']).toBe('# No content provided'); + }); + + it('should provide marked-react dependency', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '# Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.sharedProps.customSetup?.dependencies).toHaveProperty('marked-react'); + }); + + it('should update files when content changes', () => { + const artifact = createArtifact({ + type: 'text/markdown', + content: '# Original', + }); + + const { result, rerender } = renderHook(({ artifact }) => useArtifactProps({ artifact }), { + initialProps: { artifact }, + }); + + expect(result.current.files['content.md']).toBe('# Original'); + + // Update the artifact content + const updatedArtifact = createArtifact({ + ...artifact, + content: '# Updated', + }); + + rerender({ artifact: updatedArtifact }); + + expect(result.current.files['content.md']).toBe('# Updated'); + }); + }); + + describe('mermaid artifacts', () => { + it('should handle mermaid type with content.md as fileKey', () => { + const artifact = createArtifact({ + type: 'application/vnd.mermaid', + content: 'graph TD\n A-->B', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.fileKey).toBe('diagram.mmd'); + expect(result.current.template).toBe('react-ts'); + }); + }); + + describe('react artifacts', () => { + it('should handle react type with App.tsx as fileKey', () => { + const artifact = createArtifact({ + type: 'application/vnd.react', + content: 'export default () =>
Test
', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.fileKey).toBe('App.tsx'); + expect(result.current.template).toBe('react-ts'); + }); + }); + + describe('html artifacts', () => { + it('should handle html type with index.html as fileKey', () => { + const artifact = createArtifact({ + type: 'text/html', + content: 'Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + expect(result.current.fileKey).toBe('index.html'); + expect(result.current.template).toBe('static'); + }); + }); + + describe('edge cases', () => { + it('should handle artifact with language parameter', () => { + const artifact = createArtifact({ + type: 'text/markdown', + language: 'en', + content: '# Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + // Language parameter should not affect markdown handling + // It checks the type directly, not the key + expect(result.current.fileKey).toBe('content.md'); + expect(result.current.files['content.md']).toBe('# Test'); + }); + + it('should handle artifact with undefined type', () => { + const artifact = createArtifact({ + content: '# Test', + }); + + const { result } = renderHook(() => useArtifactProps({ artifact })); + + // Should use default behavior + expect(result.current.template).toBe('static'); + }); + }); +}); diff --git a/client/src/hooks/Artifacts/useArtifactProps.ts b/client/src/hooks/Artifacts/useArtifactProps.ts index 6de90f5893..2b898934c4 100644 --- a/client/src/hooks/Artifacts/useArtifactProps.ts +++ b/client/src/hooks/Artifacts/useArtifactProps.ts @@ -3,11 +3,19 @@ import { removeNullishValues } from 'librechat-data-provider'; import type { Artifact } from '~/common'; import { getKey, getProps, getTemplate, getArtifactFilename } from '~/utils/artifacts'; import { getMermaidFiles } from '~/utils/mermaid'; +import { getMarkdownFiles } from '~/utils/markdown'; export default function useArtifactProps({ artifact }: { artifact: Artifact }) { const [fileKey, files] = useMemo(() => { - if (getKey(artifact.type ?? '', artifact.language).includes('mermaid')) { - return ['App.tsx', getMermaidFiles(artifact.content ?? '')]; + const key = getKey(artifact.type ?? '', artifact.language); + const type = artifact.type ?? ''; + + if (key.includes('mermaid')) { + return ['diagram.mmd', getMermaidFiles(artifact.content ?? '')]; + } + + if (type === 'text/markdown' || type === 'text/md' || type === 'text/plain') { + return ['content.md', getMarkdownFiles(artifact.content ?? '')]; } const fileKey = getArtifactFilename(artifact.type ?? '', artifact.language); diff --git a/client/src/hooks/Artifacts/useArtifacts.ts b/client/src/hooks/Artifacts/useArtifacts.ts index 55248ffa44..5eb0d4ee73 100644 --- a/client/src/hooks/Artifacts/useArtifacts.ts +++ b/client/src/hooks/Artifacts/useArtifacts.ts @@ -122,17 +122,8 @@ export default function useArtifacts() { setCurrentArtifactId(orderedArtifactIds[newIndex]); }; - const isMermaid = useMemo(() => { - if (currentArtifact?.type == null) { - return false; - } - const key = getKey(currentArtifact.type, currentArtifact.language); - return key.includes('mermaid'); - }, [currentArtifact?.type, currentArtifact?.language]); - return { activeTab, - isMermaid, setActiveTab, currentIndex, cycleArtifact, diff --git a/client/src/utils/__tests__/markdown.test.ts b/client/src/utils/__tests__/markdown.test.ts new file mode 100644 index 0000000000..fcc0f169e6 --- /dev/null +++ b/client/src/utils/__tests__/markdown.test.ts @@ -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(' { + 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('{content}'); + }); + + 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(' = { '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 = { +const markdownDependencies = { + 'marked-react': '^2.0.0', +}; + +const dependenciesMap: Record< + | keyof typeof artifactFilename + | 'application/vnd.mermaid' + | 'text/markdown' + | 'text/md' + | 'text/plain', + Record +> = { '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, }; diff --git a/client/src/utils/markdown.ts b/client/src/utils/markdown.ts new file mode 100644 index 0000000000..12556c1a24 --- /dev/null +++ b/client/src/utils/markdown.ts @@ -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 = ({ content }) => { + return ( +
+ {content} +
+ ); +}; + +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 ; +}; + +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(); +;`), + '/components/ui/MarkdownRenderer.tsx': markdownRenderer, + 'markdown.css': markdownCSS, + }; +}; diff --git a/client/src/utils/mermaid.ts b/client/src/utils/mermaid.ts index bc95eb1f1d..7930d9ab1e 100644 --- a/client/src/utils/mermaid.ts +++ b/client/src/utils/mermaid.ts @@ -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 = () => ( + + + + + + +); + +const ZoomOut = () => ( + + + + + +); + +const RefreshCw = () => ( + + + + + + +); + interface MermaidDiagramProps { content: string; } @@ -181,21 +206,21 @@ const MermaidDiagram: React.FC = ({ content }) => {
@@ -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(); ;`), '/components/ui/MermaidDiagram.tsx': mermaid, + 'mermaid.css': mermaidCSS, }; };