WIP: artifacts first pass

This commit is contained in:
Danny Avila 2024-08-19 20:55:16 -04:00
parent 0340b4acb9
commit 24d74044e4
16 changed files with 765 additions and 12 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -0,0 +1,5 @@
export interface CodeBlock {
id: string;
language: string;
content: string;
}

View file

@ -1,2 +1,3 @@
export * from './artifacts';
export * from './types';
export * from './assistants-types';

View file

@ -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 ? (
<Sandpack
options={{
showNavigator: true,
editorHeight: '80vh',
showTabs: true,
...sharedOptions,
}}
files={{
...files,
...sharedFiles,
}}
{...sharedProps}
/>
) : (
<SandpackProvider
files={{
...files,
...sharedFiles,
}}
className="flex h-full w-full justify-center"
options={{ ...sharedOptions }}
{...sharedProps}
>
<SandpackPreview
className="flex h-full w-full justify-center"
showOpenInCodeSandbox={false}
showRefreshButton={false}
/>
</SandpackProvider>
);
}
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 (
<div className="flex h-full flex-col text-text-primary text-xl w-full rounded-xl py-2">
<div className="bg-gray-800 rounded-lg shadow-lg overflow-hidden flex flex-col h-full">
<Tabs.Root value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-full">
<Tabs.List className="flex bg-gray-700">
<Tabs.Trigger
value="code"
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white focus:outline-none focus:text-white"
>
Code
</Tabs.Trigger>
<Tabs.Trigger
value="preview"
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white focus:outline-none focus:text-white"
>
Preview
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="code" className="flex-grow overflow-auto">
<pre className="h-full bg-gray-900 p-4 rounded text-sm overflow-auto">
<code>{firstFileContent}</code>
</pre>
</Tabs.Content>
<Tabs.Content value="preview" asChild>
<CodeViewer />
</Tabs.Content>
</Tabs.Root>
</div>
</div>
);
}

View file

@ -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<HTMLElement>;
};
interface CodeBlockArtifactProps {
lang: string;
codeString: string;
artifactId: string;
}
type CodeBlockProps = Pick<CodeBarProps, 'lang'> & {
codeChildren: React.ReactNode;
classProp?: string;
};
const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef }) => {
const localize = useLocalize();
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
<button
type="button"
className='ml-auto flex gap-2'
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
setIsCopied(true);
copy(codeString, { format: 'text/plain' });
setTimeout(() => {
setIsCopied(false);
}, 3000);
}
}}
>
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{localize('com_ui_copy_code')}
</>
)}
</button>
</div>
);
});
const CodeBlock: React.FC<CodeBlockProps> = ({
lang,
codeChildren,
classProp = '',
}) => {
const codeRef = useRef<HTMLElement>(null);
return (
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
<CodeBar lang={lang} codeRef={codeRef} />
<div className={cn(classProp, 'overflow-y-auto p-4')}>
<code
ref={codeRef}
className={`hljs language-${lang} !whitespace-pre`}
>
{codeChildren}
</code>
</div>
</div>
);
};
export const CodeBlockArtifact: React.FC<CodeBlockArtifactProps> = ({ lang, codeString: content }) => {
const debouncedUpdateCodeBlock = useDebounceCodeBlock();
useEffect(() => {
debouncedUpdateCodeBlock({
id: `${lang}-${Date.now()}`,
language: lang,
content,
});
}, [lang, content, debouncedUpdateCodeBlock]);
return (
<CodePreview code={content} />
);
};
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 (
<code onDoubleClick={handleDoubleClick} className={className}>
{children}
</code>
);
}
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
});
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 (
<ReactMarkdown
rehypePlugins={rehypePlugins}
linkTarget="_new"
components={{ code }
}
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
);
});

View file

@ -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 (
<div className="group relative inline-block text-sm text-text-primary">
<div className="relative overflow-hidden rounded-xl border border-border-medium">
<div className="w-60 bg-surface-active p-2">
<div className="flex flex-row items-center gap-2">
<FilePreview fileType={fileType} className="relative" />
<div className="overflow-hidden">
<div className="truncate font-medium">{'Code Artifact'}</div>
<div className="truncate text-text-secondary">{fileType.title}</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default CodePreview;

View file

@ -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 (
<div className="calculator">
<input type="text" value={result} />
<div className="keypad">
<button className="highlight" onClick={clear} id="clear">Clear</button>
<button className="highlight" onClick={backspace} id="backspace">C</button>
<button className="highlight" name="/" onClick={handleClick}>&divide;</button>
<button name="7" onClick={handleClick}>7</button>
<button name="8" onClick={handleClick}>8</button>
<button name="9" onClick={handleClick}>9</button>
<button className="highlight" name="*" onClick={handleClick}>&times;</button>
<button name="4" onClick={handleClick}>4</button>
<button name="5" onClick={handleClick}>5</button>
<button name="6" onClick={handleClick}>6</button>
<button className="highlight" name="-" onClick={handleClick}>&ndash;</button>
<button name="1" onClick={handleClick}>1</button>
<button name="2" onClick={handleClick}>2</button>
<button name="3" onClick={handleClick}>3</button>
<button className="highlight" name="+" onClick={handleClick}>+</button>
<button name="0" onClick={handleClick}>0</button>
<button name="." onClick={handleClick}>.</button>
<button className="highlight" onClick={calculate} id="result">=</button>
</div>
</div>
);
}
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 ? (
<Sandpack
options={{
showNavigator: true,
editorHeight: '80vh',
showTabs: true,
...sharedOptions,
}}
files={{
// 'App.tsx': code,
'App.tsx': App,
...sharedFiles,
'styles.css': styles,
}}
{...sharedProps}
/>
) : (
<SandpackProvider
files={{
// 'App.tsx': code,
'App.tsx': App,
...sharedFiles,
'styles.css': styles,
}}
className="flex h-full w-full grow flex-col justify-center"
options={{ ...sharedOptions }}
{...sharedProps}
>
<SandpackPreview
className="flex h-full w-full grow flex-col justify-center p-4 md:pt-16"
showOpenInCodeSandbox={false}
showRefreshButton={false}
/>
</SandpackProvider>
);
}
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`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
`,
};

View file

@ -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;
}

View file

@ -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<number | null>;
};
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}
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
}
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 <CodeMarkdown content={`\`\`\`${lang}\n${codeString}\n\`\`\``} {...props}/>;
} else if (codeArtifacts && typeof codeString === 'string') {
return <CodeBlockArtifact lang={lang ?? 'text'} codeString={codeString} {...props}/>;
}
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
});
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 <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
@ -114,7 +132,10 @@ type TContentProps = {
};
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
const artifactIdRef = useRef<string | null>(null);
const codeBlocksRef = useRef<number | null>(null);
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
const codeArtifacts = useRecoilValue<boolean>(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;
}
}
>

View file

@ -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 ? <Artifacts /> : null}
>
<main className="flex h-full flex-col" role="main">
{children}

View file

@ -75,7 +75,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
return (
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
<CodeBar lang={lang} codeRef={codeRef} plugin={plugin === true} error={error} />
<CodeBar lang={lang} codeRef={codeRef} plugin={(plugin === true ?? false)} error={error} />
<div className={cn(classProp, 'overflow-y-auto p-4')}>
<code
ref={codeRef}

View file

@ -23,6 +23,7 @@ interface SidePanelProps {
defaultCollapsed?: boolean;
navCollapsedSize?: number;
fullPanelCollapse?: boolean;
artifacts?: React.ReactNode;
children: React.ReactNode;
}
@ -34,6 +35,7 @@ const SidePanel = ({
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
const localize = useLocalize();
@ -133,6 +135,8 @@ const SidePanel = ({
}
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return (
<>
<TooltipProvider delayDuration={0}>
@ -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"
>
<ResizablePanel defaultSize={defaultLayout[0]} minSize={30}>
<ResizablePanel defaultSize={defaultLayout[0]} minSize={minSizeMain}>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="bg-border-light dark:text-white mx-4" />
<ResizablePanel defaultSize={defaultLayout[0]} minSize={minSizeMain}>
{artifacts}
</ResizablePanel>
</>
)}
<TooltipProvider delayDuration={400}>
<Tooltip>
<div

View file

@ -312,3 +312,11 @@
.chrome-scrollbar::-webkit-scrollbar-track {
background-color: transparent; /* Color of the tracking area */
}
.sp-preview-container {
@apply flex h-full w-full grow flex-col justify-center;
}
.sp-preview-iframe {
@apply grow;
}

View file

@ -0,0 +1,27 @@
// client/src/store/artifacts.ts
import { atom } from 'recoil';
import type { CodeBlock } from '~/common';
export const codeBlocksState = atom<Record<string, CodeBlock>>({
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<string[]>({
key: 'codeBlockIdsState',
default: [],
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
console.log('Recoil Effect: Setting codeBlockIdsState', { key: node.key, newValue });
});
},
] as const,
});

View file

@ -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,

View file

@ -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`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
`,
};
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<string, CodeBlock | undefined>) => {
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<string, string>);
};