diff --git a/bun.lockb b/bun.lockb
index 1351a98068..e85113bbce 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/client/src/common/artifacts.ts b/client/src/common/artifacts.ts
new file mode 100644
index 0000000000..e472af5b64
--- /dev/null
+++ b/client/src/common/artifacts.ts
@@ -0,0 +1,5 @@
+export interface CodeBlock {
+ id: string;
+ language: string;
+ content: string;
+}
\ No newline at end of file
diff --git a/client/src/common/index.ts b/client/src/common/index.ts
index 35acc738ee..2d56ecef51 100644
--- a/client/src/common/index.ts
+++ b/client/src/common/index.ts
@@ -1,2 +1,3 @@
+export * from './artifacts';
export * from './types';
export * from './assistants-types';
diff --git a/client/src/components/Artifacts/Artifacts.tsx b/client/src/components/Artifacts/Artifacts.tsx
new file mode 100644
index 0000000000..b5d7713add
--- /dev/null
+++ b/client/src/components/Artifacts/Artifacts.tsx
@@ -0,0 +1,94 @@
+import React, { useMemo, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import * as Tabs from '@radix-ui/react-tabs';
+import { Sandpack } from '@codesandbox/sandpack-react';
+import {
+ SandpackPreview,
+ SandpackProvider,
+} from '@codesandbox/sandpack-react/unstyled';
+import { mapCodeFiles, sharedOptions, sharedFiles, sharedProps } from '~/utils/artifacts';
+import store from '~/store';
+
+export function CodeViewer({ showEditor = false }: { showEditor?: boolean }) {
+ const codeBlockIds = useRecoilValue(store.codeBlockIdsState);
+ const codeBlocks = useRecoilValue(store.codeBlocksState);
+
+ const files = useMemo(() => mapCodeFiles(codeBlockIds, codeBlocks), [codeBlockIds, codeBlocks]);
+
+ console.log('CODE FILES & blocks', files, codeBlocks);
+ if ((Object.keys(files)).length === 0) {
+ return null;
+ }
+
+ return showEditor ? (
+
+ ) : (
+
+
+
+ );
+}
+
+export default function Artifacts() {
+ const [activeTab, setActiveTab] = useState('code');
+ const codeBlockIds = useRecoilValue(store.codeBlockIdsState);
+ const codeBlocks = useRecoilValue(store.codeBlocksState);
+
+ const files = useMemo(() => mapCodeFiles(codeBlockIds, codeBlocks), [codeBlockIds, codeBlocks]);
+ const firstFileContent = Object.values(files)[0] || '';
+
+ return (
+
+
+
+
+
+ Code
+
+
+ Preview
+
+
+
+
+ {firstFileContent}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/Artifacts/Code.tsx b/client/src/components/Artifacts/Code.tsx
new file mode 100644
index 0000000000..6f86b90179
--- /dev/null
+++ b/client/src/components/Artifacts/Code.tsx
@@ -0,0 +1,155 @@
+import React, { useRef, useState, RefObject, memo, useEffect } from 'react';
+
+import copy from 'copy-to-clipboard';
+import rehypeKatex from 'rehype-katex';
+import ReactMarkdown from 'react-markdown';
+import type { PluggableList } from 'unified';
+import rehypeHighlight from 'rehype-highlight';
+import { useDebounceCodeBlock } from './useDebounceCodeBlock';
+import { handleDoubleClick, cn, langSubset } from '~/utils';
+import Clipboard from '~/components/svg/Clipboard';
+import CheckMark from '~/components/svg/CheckMark';
+import useLocalize from '~/hooks/useLocalize';
+import CodePreview from './CodePreview';
+
+type CodeBarProps = {
+ lang: string;
+ codeRef: RefObject;
+};
+
+interface CodeBlockArtifactProps {
+ lang: string;
+ codeString: string;
+ artifactId: string;
+}
+type CodeBlockProps = Pick & {
+ codeChildren: React.ReactNode;
+ classProp?: string;
+};
+
+const CodeBar: React.FC = React.memo(({ lang, codeRef }) => {
+ const localize = useLocalize();
+ const [isCopied, setIsCopied] = useState(false);
+ return (
+
+ {lang}
+
+
+ );
+});
+
+const CodeBlock: React.FC = ({
+ lang,
+ codeChildren,
+ classProp = '',
+}) => {
+ const codeRef = useRef(null);
+ return (
+
+
+
+
+ {codeChildren}
+
+
+
+ );
+};
+
+export const CodeBlockArtifact: React.FC = ({ lang, codeString: content }) => {
+ const debouncedUpdateCodeBlock = useDebounceCodeBlock();
+
+ useEffect(() => {
+ debouncedUpdateCodeBlock({
+ id: `${lang}-${Date.now()}`,
+ language: lang,
+ content,
+ });
+ }, [lang, content, debouncedUpdateCodeBlock]);
+
+ return (
+
+ );
+};
+
+type TCodeProps = {
+ inline: boolean;
+ className?: string;
+ children: React.ReactNode;
+};
+
+export const code: React.ElementType = memo(({ inline, className, children }: TCodeProps) => {
+ const match = /language-(\w+)/.exec(className ?? '');
+ const lang = match && match[1];
+
+ if (inline) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return ;
+});
+
+const cursor = ' ';
+export const CodeMarkdown = memo(({ content = '', showCursor, isLatestMessage }: {
+ content: string;
+ showCursor?: boolean;
+ isLatestMessage: boolean;
+}) => {
+
+ const currentContent = content;
+ const rehypePlugins: PluggableList = [
+ [rehypeKatex, { output: 'mathml' }],
+ [
+ rehypeHighlight,
+ {
+ detect: true,
+ ignoreMissing: true,
+ subset: langSubset,
+ },
+ ],
+ ];
+
+ return (
+
+ {isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
+
+ );
+});
\ No newline at end of file
diff --git a/client/src/components/Artifacts/CodePreview.tsx b/client/src/components/Artifacts/CodePreview.tsx
new file mode 100644
index 0000000000..2a80bdeddd
--- /dev/null
+++ b/client/src/components/Artifacts/CodePreview.tsx
@@ -0,0 +1,29 @@
+import FilePreview from '~/components/Chat/Input/Files/FilePreview';
+import { getFileType } from '~/utils';
+
+const CodePreview = ({
+ code,
+}: {
+ code: string,
+ onDelete?: () => void;
+}) => {
+ const fileType = getFileType('text/x-');
+
+ return (
+
+
+
+
+
+
+
{'Code Artifact'}
+
{fileType.title}
+
+
+
+
+
+ );
+};
+
+export default CodePreview;
diff --git a/client/src/components/Artifacts/example.tsx b/client/src/components/Artifacts/example.tsx
new file mode 100644
index 0000000000..a710b3d863
--- /dev/null
+++ b/client/src/components/Artifacts/example.tsx
@@ -0,0 +1,230 @@
+import dedent from 'dedent';
+import { Sandpack } from '@codesandbox/sandpack-react';
+import {
+ SandpackPreview,
+ SandpackProvider,
+} from '@codesandbox/sandpack-react/unstyled';
+// import './code-viewer.css';
+
+const App = `import React, { useState } from 'react';
+import './styles.css';
+
+function App() {
+ const [result, setResult] = useState('');
+
+ const handleClick = (e) => {
+ setResult(result.concat(e.target.name));
+ }
+
+ const clear = () => {
+ setResult('');
+ }
+
+ const backspace = () => {
+ setResult(result.slice(0, -1));
+ }
+
+ const calculate = () => {
+ try {
+ setResult(eval(result).toString());
+ } catch(err) {
+ setResult('Error');
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;`;
+
+const styles = `
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.calculator {
+ width: 320px;
+ margin: 100px auto;
+}
+
+input[type="text"] {
+ width: 100%;
+ height: 60px;
+ font-size: 20px;
+ text-align: right;
+ padding: 0 10px;
+ pointer-events: none;
+}
+
+.keypad {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-gap: 10px;
+ padding: 10px;
+}
+
+button {
+ width: 100%;
+ height: 60px;
+ font-size: 18px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ background-color: #f0f0f0;
+}
+
+button:hover {
+ background-color: #ddd;
+}
+
+.highlight {
+ background-color: #ff8c00;
+ color: white;
+}
+
+.highlight:hover {
+ background-color: #e67e00;
+}
+`;
+
+export function DevCodeViewer({
+ code,
+ showEditor = false,
+}: {
+ code?: string;
+ showEditor?: boolean;
+}) {
+ return showEditor ? (
+
+ ) : (
+
+
+
+ );
+}
+
+const sharedProps = {
+ template: 'react-ts',
+ // theme: draculaTheme,
+ customSetup: {
+ dependencies: {
+ 'lucide-react': '^0.394.0',
+ 'react-router-dom': '^6.11.2',
+ 'class-variance-authority': '^0.6.0',
+ clsx: '^1.2.1',
+ 'date-fns': '^3.3.1',
+ 'tailwind-merge': '^1.9.1',
+ 'tailwindcss-animate': '^1.0.5',
+ // recharts: '2.9.0',
+ // '@radix-ui/react-accordion': '^1.2.0',
+ // '@radix-ui/react-alert-dialog': '^1.1.1',
+ // '@radix-ui/react-aspect-ratio': '^1.1.0',
+ // '@radix-ui/react-avatar': '^1.1.0',
+ // '@radix-ui/react-checkbox': '^1.1.1',
+ // '@radix-ui/react-collapsible': '^1.1.0',
+ // '@radix-ui/react-dialog': '^1.1.1',
+ // '@radix-ui/react-dropdown-menu': '^2.1.1',
+ // '@radix-ui/react-hover-card': '^1.1.1',
+ // '@radix-ui/react-label': '^2.1.0',
+ // '@radix-ui/react-menubar': '^1.1.1',
+ // '@radix-ui/react-navigation-menu': '^1.2.0',
+ // '@radix-ui/react-popover': '^1.1.1',
+ // '@radix-ui/react-progress': '^1.1.0',
+ // '@radix-ui/react-radio-group': '^1.2.0',
+ // '@radix-ui/react-select': '^2.1.1',
+ // '@radix-ui/react-separator': '^1.1.0',
+ // '@radix-ui/react-slider': '^1.2.0',
+ // '@radix-ui/react-slot': '^1.1.0',
+ // '@radix-ui/react-switch': '^1.1.0',
+ // '@radix-ui/react-tabs': '^1.1.0',
+ // '@radix-ui/react-toast': '^1.2.1',
+ // '@radix-ui/react-toggle': '^1.1.0',
+ // '@radix-ui/react-toggle-group': '^1.1.0',
+ // '@radix-ui/react-tooltip': '^1.1.2',
+
+ // 'embla-carousel-react': '^8.1.8',
+ // 'react-day-picker': '^8.10.1',
+ // vaul: '^0.9.1',
+ },
+ },
+} as const;
+
+const sharedOptions = {
+ externalResources: [
+ 'https://unpkg.com/@tailwindcss/ui/dist/tailwind-ui.min.css',
+ ],
+};
+
+const sharedFiles = {
+ '/public/index.html': dedent`
+
+
+
+
+
+ Document
+
+
+
+
+
+
+ `,
+};
\ No newline at end of file
diff --git a/client/src/components/Artifacts/useDebounceCodeBlock.ts b/client/src/components/Artifacts/useDebounceCodeBlock.ts
new file mode 100644
index 0000000000..27aaf5bc83
--- /dev/null
+++ b/client/src/components/Artifacts/useDebounceCodeBlock.ts
@@ -0,0 +1,37 @@
+// client/src/hooks/useDebounceCodeBlock.ts
+import { useCallback, useEffect } from 'react';
+import debounce from 'lodash/debounce';
+import { useSetRecoilState } from 'recoil';
+import { codeBlocksState, codeBlockIdsState } from '~/store/artifacts';
+import type { CodeBlock } from '~/common';
+
+export function useDebounceCodeBlock() {
+ const setCodeBlocks = useSetRecoilState(codeBlocksState);
+ const setCodeBlockIds = useSetRecoilState(codeBlockIdsState);
+
+ const updateCodeBlock = useCallback((codeBlock: CodeBlock) => {
+ console.log('Updating code block:', codeBlock);
+ setCodeBlocks((prev) => ({
+ ...prev,
+ [codeBlock.id]: codeBlock,
+ }));
+ setCodeBlockIds((prev) =>
+ prev.includes(codeBlock.id) ? prev : [...prev, codeBlock.id],
+ );
+ }, [setCodeBlocks, setCodeBlockIds]);
+
+ const debouncedUpdateCodeBlock = useCallback(
+ debounce((codeBlock: CodeBlock) => {
+ updateCodeBlock(codeBlock);
+ }, 25),
+ [updateCodeBlock],
+ );
+
+ useEffect(() => {
+ return () => {
+ debouncedUpdateCodeBlock.cancel();
+ };
+ }, [debouncedUpdateCodeBlock]);
+
+ return debouncedUpdateCodeBlock;
+}
diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx
index 2765543fb4..8bcf656d4c 100644
--- a/client/src/components/Chat/Messages/Content/Markdown.tsx
+++ b/client/src/components/Chat/Messages/Content/Markdown.tsx
@@ -1,15 +1,18 @@
-import React, { memo, useMemo } from 'react';
+import React, { memo, RefObject, useMemo, useRef } from 'react';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import rehypeKatex from 'rehype-katex';
import { useRecoilValue } from 'recoil';
+import { visit } from 'unist-util-visit';
import ReactMarkdown from 'react-markdown';
-import type { PluggableList } from 'unified';
import rehypeHighlight from 'rehype-highlight';
+import type { PluggableList, Pluggable } from 'unified';
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
+import { CodeBlockArtifact, CodeMarkdown } from '~/components/Artifacts/Code';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useFileDownload } from '~/data-provider';
+import { filenameMap } from '~/utils/artifacts';
import useLocalize from '~/hooks/useLocalize';
import { useToastContext } from '~/Providers';
import store from '~/store';
@@ -18,9 +21,14 @@ type TCodeProps = {
inline: boolean;
className?: string;
children: React.ReactNode;
+ isLatestMessage: boolean;
+ showCursor?: boolean;
+ artifactId: string;
+ codeBlocksRef: RefObject;
};
-export const code: React.ElementType = memo(({ inline, className, children }: TCodeProps) => {
+export const code: React.ElementType = memo(({ inline, className, children, ...props }: TCodeProps) => {
+ const codeArtifacts = useRecoilValue(store.codeArtifacts);
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
@@ -30,12 +38,22 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC
{children}
);
- } else {
- return ;
}
+
+ const codeString = Array.isArray(children) ? children.join('') : children;
+ console.log('code lang, children, props', lang, children, props);
+ const isNonArtifact = filenameMap[lang ?? ''] === undefined;
+
+ if (codeArtifacts && typeof codeString === 'string' && isNonArtifact) {
+ return ;
+ } else if (codeArtifacts && typeof codeString === 'string') {
+ return ;
+ }
+
+ return ;
});
-export const a = memo(({ href, children }: { href: string; children: React.ReactNode }) => {
+export const a: React.ElementType = memo(({ href, children }: { href: string; children: React.ReactNode }) => {
const user = useRecoilValue(store.user);
const { showToast } = useToastContext();
const localize = useLocalize();
@@ -101,7 +119,7 @@ export const a = memo(({ href, children }: { href: string; children: React.React
);
});
-export const p = memo(({ children }: { children: React.ReactNode }) => {
+export const p: React.ElementType = memo(({ children }: { children: React.ReactNode }) => {
return {children}
;
});
@@ -114,7 +132,10 @@ type TContentProps = {
};
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
+ const artifactIdRef = useRef(null);
+ const codeBlocksRef = useRef(null);
const LaTeXParsing = useRecoilValue(store.LaTeXParsing);
+ const codeArtifacts = useRecoilValue(store.codeArtifacts);
const isInitializing = content === '';
@@ -124,7 +145,25 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
currentContent = LaTeXParsing ? preprocessLaTeX(currentContent) : currentContent;
}
- const rehypePlugins: PluggableList = [
+ if (artifactIdRef.current === null) {
+ artifactIdRef.current = new Date().toISOString();
+ }
+
+ const codePlugin: Pluggable = () => {
+ return (tree) => {
+ visit(tree, { tagName: 'code' }, (node) => {
+ node.properties = {
+ ...node.properties,
+ isLatestMessage,
+ showCursor,
+ artifactId: artifactIdRef.current,
+ codeBlocksRef: codeBlocksRef.current,
+ };
+ });
+ };
+ };
+
+ const rehypePlugins: PluggableList = codeArtifacts ? [[rehypeKatex, { output: 'mathml' }], [codePlugin], [rehypeRaw]] : [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
@@ -156,8 +195,6 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
code,
a,
p,
- } as {
- [nodeType: string]: React.ElementType;
}
}
>
diff --git a/client/src/components/Chat/Presentation.tsx b/client/src/components/Chat/Presentation.tsx
index 9fb743e7aa..76304d4336 100644
--- a/client/src/components/Chat/Presentation.tsx
+++ b/client/src/components/Chat/Presentation.tsx
@@ -6,6 +6,7 @@ import type { ExtendedFile } from '~/common';
import { useDragHelpers, useSetFilesToDelete } from '~/hooks';
import DragDropOverlay from './Input/Files/DragDropOverlay';
import { useDeleteFilesMutation } from '~/data-provider';
+import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel';
import store from '~/store';
@@ -21,7 +22,9 @@ export default function Presentation({
useSidePanel?: boolean;
}) {
const { data: startupConfig } = useGetStartupConfig();
+ const codeArtifacts = useRecoilValue(store.codeArtifacts);
const hideSidePanel = useRecoilValue(store.hideSidePanel);
+ const codeBlockIds = useRecoilValue(store.codeBlockIdsState);
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
@@ -89,6 +92,7 @@ export default function Presentation({
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
+ artifacts={codeArtifacts && codeBlockIds.length > 0 ? : null}
>
{children}
diff --git a/client/src/components/Messages/Content/CodeBlock.tsx b/client/src/components/Messages/Content/CodeBlock.tsx
index 7f1b4e1b39..7e14443d95 100644
--- a/client/src/components/Messages/Content/CodeBlock.tsx
+++ b/client/src/components/Messages/Content/CodeBlock.tsx
@@ -75,7 +75,7 @@ const CodeBlock: React.FC = ({
return (
-
+
{
const localize = useLocalize();
@@ -133,6 +135,8 @@ const SidePanel = ({
}
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
+ const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
+
return (
<>
@@ -141,9 +145,17 @@ const SidePanel = ({
onLayout={(sizes: number[]) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-white dark:bg-gray-800"
>
-
+
{children}
+ {artifacts != null && (
+ <>
+
+
+ {artifacts}
+
+ >
+ )}
>({
+ key: 'codeBlocksState',
+ default: {},
+ effects: [
+ ({ onSet, node }) => {
+ onSet(async (newValue) => {
+ console.log('Recoil Effect: Setting codeBlocksState', { key: node.key, newValue });
+ });
+ },
+ ] as const,
+});
+
+export const codeBlockIdsState = atom
({
+ key: 'codeBlockIdsState',
+ default: [],
+ effects: [
+ ({ onSet, node }) => {
+ onSet(async (newValue) => {
+ console.log('Recoil Effect: Setting codeBlockIdsState', { key: node.key, newValue });
+ });
+ },
+ ] as const,
+});
\ No newline at end of file
diff --git a/client/src/store/index.ts b/client/src/store/index.ts
index 445690ab2a..291dc2083e 100644
--- a/client/src/store/index.ts
+++ b/client/src/store/index.ts
@@ -1,3 +1,4 @@
+import * as artifacts from './artifacts';
import conversation from './conversation';
import conversations from './conversations';
import families from './families';
@@ -13,6 +14,7 @@ import lang from './language';
import settings from './settings';
export default {
+ ...artifacts,
...families,
...conversation,
...conversations,
diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts
new file mode 100644
index 0000000000..4c5f943f94
--- /dev/null
+++ b/client/src/utils/artifacts.ts
@@ -0,0 +1,112 @@
+import dedent from 'dedent';
+import type { CodeBlock } from '~/common';
+
+export function getFileExtension(language: string): string {
+ switch (language) {
+ case 'javascript':
+ return 'jsx';
+ case 'typescript':
+ return 'tsx';
+ case 'jsx':
+ return 'jsx';
+ case 'tsx':
+ return 'tsx';
+ case 'html':
+ return 'html';
+ case 'css':
+ return 'css';
+ default:
+ return 'txt';
+ }
+}
+
+export const sharedProps = {
+ template: 'react-ts',
+ customSetup: {
+ dependencies: {
+ 'lucide-react': '^0.394.0',
+ 'react-router-dom': '^6.11.2',
+ 'class-variance-authority': '^0.6.0',
+ clsx: '^1.2.1',
+ 'date-fns': '^3.3.1',
+ 'tailwind-merge': '^1.9.1',
+ 'tailwindcss-animate': '^1.0.5',
+ // recharts: '2.9.0',
+ // '@radix-ui/react-accordion': '^1.2.0',
+ // '@radix-ui/react-alert-dialog': '^1.1.1',
+ // '@radix-ui/react-aspect-ratio': '^1.1.0',
+ // '@radix-ui/react-avatar': '^1.1.0',
+ // '@radix-ui/react-checkbox': '^1.1.1',
+ // '@radix-ui/react-collapsible': '^1.1.0',
+ // '@radix-ui/react-dialog': '^1.1.1',
+ // '@radix-ui/react-dropdown-menu': '^2.1.1',
+ // '@radix-ui/react-hover-card': '^1.1.1',
+ // '@radix-ui/react-label': '^2.1.0',
+ // '@radix-ui/react-menubar': '^1.1.1',
+ // '@radix-ui/react-navigation-menu': '^1.2.0',
+ // '@radix-ui/react-popover': '^1.1.1',
+ // '@radix-ui/react-progress': '^1.1.0',
+ // '@radix-ui/react-radio-group': '^1.2.0',
+ // '@radix-ui/react-select': '^2.1.1',
+ // '@radix-ui/react-separator': '^1.1.0',
+ // '@radix-ui/react-slider': '^1.2.0',
+ // '@radix-ui/react-slot': '^1.1.0',
+ // '@radix-ui/react-switch': '^1.1.0',
+ // '@radix-ui/react-tabs': '^1.1.0',
+ // '@radix-ui/react-toast': '^1.2.1',
+ // '@radix-ui/react-toggle': '^1.1.0',
+ // '@radix-ui/react-toggle-group': '^1.1.0',
+ // '@radix-ui/react-tooltip': '^1.1.2',
+
+ // 'embla-carousel-react': '^8.1.8',
+ // 'react-day-picker': '^8.10.1',
+ // vaul: '^0.9.1',
+ },
+ },
+} as const;
+
+export const sharedOptions = {
+ externalResources: [
+ 'https://unpkg.com/@tailwindcss/ui/dist/tailwind-ui.min.css',
+ ],
+};
+
+export const sharedFiles = {
+ '/public/index.html': dedent`
+
+
+
+
+
+ Document
+
+
+
+
+
+
+ `,
+};
+
+export const filenameMap = {
+ 'tsx': 'App',
+ 'css': 'styles',
+ 'html': 'index',
+ 'jsx': 'App',
+ 'js': 'App',
+ 'ts': 'App',
+ 'typescript': 'App',
+ 'javascript': 'App',
+};
+
+export const mapCodeFiles = (codeBlockIds:string[], codeBlocks: Record) => {
+
+ return codeBlockIds.reduce((acc, id) => {
+ const block = codeBlocks[id];
+ if (block) {
+ const fileName = `${filenameMap[block.language]}.${getFileExtension(block.language)}`;
+ acc[fileName] = typeof block.content === 'string' ? block.content : '';
+ }
+ return acc;
+ }, {} as Record);
+};
\ No newline at end of file