feat: second pass

This commit is contained in:
Danny Avila 2024-08-23 08:03:38 -04:00
parent 6ee70acdc5
commit 0a54489842
9 changed files with 163 additions and 121 deletions

View file

@ -2,4 +2,13 @@ export interface CodeBlock {
id: string;
language: string;
content: string;
}
}
export interface Artifact {
id: string;
identifier?: string;
language?: string;
content?: string;
title?: string;
type?: string;
}

View file

@ -1,9 +1,11 @@
import { useEffect, useCallback, useRef } from 'react';
import React, { useEffect, useCallback, useRef, useState } from 'react';
import throttle from 'lodash/throttle';
import { visit } from 'unist-util-visit';
import { useSetRecoilState } from 'recoil';
import { artifactsState, artifactIdsState } from '~/store/artifacts';
import type { Pluggable } from 'unified';
import throttle from 'lodash/throttle';
import type { Artifact } from '~/common';
import { artifactsState, artifactIdsState } from '~/store/artifacts';
import CodePreview from './CodePreview';
export const artifactPlugin: Pluggable = () => {
return (tree) => {
@ -18,9 +20,32 @@ export const artifactPlugin: Pluggable = () => {
};
};
export function Artifact({ node, ...props }) {
const extractContent = (
children: React.ReactNode | { props: { children: React.ReactNode } } | string,
): string => {
if (typeof children === 'string') {
return children;
}
if (React.isValidElement(children)) {
return extractContent((children.props as { children?: React.ReactNode }).children);
}
if (Array.isArray(children)) {
return children.map(extractContent).join('');
}
return '';
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Artifact({
node,
...props
}: Artifact & {
children: React.ReactNode | { props: { children: React.ReactNode } };
node: unknown;
}) {
const setArtifacts = useSetRecoilState(artifactsState);
const setArtifactIds = useSetRecoilState(artifactIdsState);
const [artifact, setArtifact] = useState<Artifact | null>(null);
const throttledUpdateRef = useRef(
throttle((updateFn: () => void) => {
@ -29,31 +54,34 @@ export function Artifact({ node, ...props }) {
);
const updateArtifact = useCallback(() => {
const content =
props.children && typeof props.children === 'string'
? props.children
: props.children?.props?.children || '';
const content = extractContent(props.children);
console.log('Content:', content);
const title = props.title || 'Untitled Artifact';
const type = props.type || 'unknown';
const identifier = props.identifier || 'no-identifier';
const title = props.title ?? 'Untitled Artifact';
const type = props.type ?? 'unknown';
const identifier = props.identifier ?? 'no-identifier';
const artifactKey = `${identifier}_${type}_${title}`.replace(/\s+/g, '_').toLowerCase();
throttledUpdateRef.current(() => {
const currentArtifact = {
id: artifactKey,
identifier,
title,
type,
content,
};
setArtifacts((prevArtifacts) => {
if (prevArtifacts[artifactKey] && prevArtifacts[artifactKey].content === content) {
if (
(prevArtifacts as Record<string, Artifact | undefined>)[artifactKey] &&
prevArtifacts[artifactKey].content === content
) {
return prevArtifacts;
}
return {
...prevArtifacts,
[artifactKey]: {
id: artifactKey,
identifier,
title,
type,
content,
},
[artifactKey]: currentArtifact,
};
});
@ -63,19 +91,28 @@ export function Artifact({ node, ...props }) {
}
return prevIds;
});
setArtifact(currentArtifact);
});
}, [props, setArtifacts, setArtifactIds]);
console.log('Artifact updated:', artifactKey);
}, [props.children, props.title, props.type, props.identifier, setArtifacts, setArtifactIds]);
useEffect(() => {
updateArtifact();
}, [updateArtifact]);
return (
<div className="artifact">
<b>{props.title || 'Untitled Artifact'}</b>
<p>Type: {props.type || 'unknown'}</p>
<p>Identifier: {props.identifier || 'No identifier'}</p>
{props.children}
</div>
<>
<CodePreview artifact={artifact} />
{/* {props.children} */}
</>
);
}
// <div className="artifact">
// <b>{props.title ?? 'Untitled Artifact'}</b>
// <p>Type: {props.type ?? 'unknown'}</p>
// <p>Identifier: {props.identifier ?? 'No identifier'}</p>
// {props.children as React.ReactNode}
// </div>

View file

@ -2,18 +2,22 @@ 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 { removeNullishValues } from 'librechat-data-provider';
import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled';
import { sharedOptions, sharedFiles, sharedProps } from '~/utils/artifacts';
import { sharedOptions, sharedFiles, sharedProps, getArtifactFilename } from '~/utils/artifacts';
import type { Artifact } from '~/common';
import store from '~/store';
export function CodeViewer({
export function ArtifactPreview({
showEditor = false,
content,
artifact,
}: {
showEditor?: boolean;
content: string;
artifact: Artifact;
}) {
const files = { '/App.js': content };
const files = useMemo(() => {
return removeNullishValues({ [getArtifactFilename(artifact.type ?? '')]: artifact.content });
}, [artifact.type, artifact.content]);
if (Object.keys(files).length === 0) {
return null;
@ -128,11 +132,11 @@ export default function Artifacts() {
{/* Content */}
<Tabs.Content value="code" className="flex-grow overflow-auto bg-surface-secondary">
<pre className="h-full w-full overflow-auto rounded bg-surface-primary-alt p-2 text-sm">
<code>{currentArtifact.content}</code>
{currentArtifact.content}
</pre>
</Tabs.Content>
<Tabs.Content value="preview" className="flex-grow overflow-auto bg-surface-secondary">
<CodeViewer content={currentArtifact.content} />
<ArtifactPreview artifact={currentArtifact} />
</Tabs.Content>
{/* Footer */}

View file

@ -35,7 +35,7 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef }) => {
<span className="">{lang}</span>
<button
type="button"
className='ml-auto flex gap-2'
className="ml-auto flex gap-2"
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
@ -64,20 +64,13 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef }) => {
);
});
const CodeBlock: React.FC<CodeBlockProps> = ({
lang,
codeChildren,
classProp = '',
}) => {
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`}
>
<code ref={codeRef} className={`hljs language-${lang} !whitespace-pre`}>
{codeChildren}
</code>
</div>
@ -85,22 +78,6 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
);
};
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;
@ -123,33 +100,37 @@ export const code: React.ElementType = memo(({ inline, className, children }: TC
});
const cursor = ' ';
export const CodeMarkdown = memo(({ content = '', showCursor, isLatestMessage }: {
content: string;
showCursor?: boolean;
isLatestMessage: boolean;
}) => {
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,
},
],
];
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>
);
});
return (
<ReactMarkdown
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={{ code }}
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
);
},
);

View file

@ -1,12 +1,11 @@
import type { Artifact } from '~/common';
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { getFileType } from '~/utils';
const CodePreview = ({
code,
}: {
code: string,
onDelete?: () => void;
}) => {
const CodePreview = ({ artifact }: { artifact: Artifact | null }) => {
if (!artifact) {
return null;
}
const fileType = getFileType('text/x-');
return (
@ -16,7 +15,7 @@ const CodePreview = ({
<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 font-medium">{artifact.title}</div>
<div className="truncate text-text-secondary">{fileType.title}</div>
</div>
</div>

View file

@ -10,7 +10,7 @@ import remarkDirective from 'remark-directive';
import type { PluggableList, Pluggable } from 'unified';
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
import { CodeBlockArtifact, CodeMarkdown } from '~/components/Artifacts/Code';
import { Artifact as artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
@ -173,7 +173,7 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
code,
a,
p,
artifact,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
}

View file

@ -47,12 +47,15 @@ export default function Presentation({
const filesToDelete = localStorage.getItem(LocalStorageKeys.FILES_TO_DELETE);
const map = JSON.parse(filesToDelete ?? '{}') as Record<string, ExtendedFile>;
const files = Object.values(map)
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
.filter(
(file) =>
file.filepath != null && file.source && !(file.embedded ?? false) && file.temp_file_id,
)
.map((file) => ({
file_id: file.file_id,
filepath: file.filepath as string,
source: file.source as FileSources,
embedded: !!file.embedded,
embedded: !!(file.embedded ?? false),
}));
if (files.length === 0) {
@ -106,7 +109,7 @@ export default function Presentation({
return (
<div ref={drop} className="relative flex w-full grow overflow-hidden bg-white dark:bg-gray-800">
{layout()}
{panel && panel}
{panel != null && panel}
</div>
);
}

View file

@ -1,11 +1,6 @@
import { atom } from 'recoil';
import { logger } from '~/utils';
export interface Artifact {
identifier?: string;
title: string;
type: string;
content: string;
}
import type { Artifact } from '~/common';
export const artifactsState = atom<Record<string, Artifact>>({
key: 'artifactsState',

View file

@ -1,6 +1,20 @@
import dedent from 'dedent';
import type { CodeBlock } from '~/common';
const artifactFilename = {
'application/vnd.react': 'App.tsx',
'text/html': 'index.html',
// 'css': 'css',
// 'javascript': 'js',
// 'typescript': 'ts',
// 'jsx': 'jsx',
// 'tsx': 'tsx',
};
export function getArtifactFilename(type: string): string {
return artifactFilename[type] ?? 'App.tsx';
}
export function getFileExtension(language: string): string {
switch (language) {
case 'javascript':
@ -66,9 +80,7 @@ export const sharedProps = {
} as const;
export const sharedOptions = {
externalResources: [
'https://unpkg.com/@tailwindcss/ui/dist/tailwind-ui.min.css',
],
externalResources: ['https://unpkg.com/@tailwindcss/ui/dist/tailwind-ui.min.css'],
};
export const sharedFiles = {
@ -89,18 +101,20 @@ export const sharedFiles = {
};
export const filenameMap = {
'tsx': 'App',
'css': 'styles',
'html': 'index',
'jsx': 'App',
'js': 'App',
'ts': 'App',
'typescript': 'App',
'javascript': 'App',
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>) => {
export const mapCodeFiles = (
codeBlockIds: string[],
codeBlocks: Record<string, CodeBlock | undefined>,
) => {
return codeBlockIds.reduce((acc, id) => {
const block = codeBlocks[id];
if (block) {
@ -109,4 +123,4 @@ export const mapCodeFiles = (codeBlockIds:string[], codeBlocks: Record<string, C
}
return acc;
}, {} as Record<string, string>);
};
};