LibreChat/client/src/components/Artifacts/ArtifactCodeEditor.tsx
Danny Avila ed57bb4711
🚀 feat: Artifact Editing & Downloads (#5428)
* refactor: expand container

* chore: bump @codesandbox/sandpack-react to latest

* WIP: first pass, show editor

* feat: implement ArtifactCodeEditor and ArtifactTabs components for enhanced artifact management

* refactor: fileKey

* refactor: auto scrolling code editor and add messageId to artifact

* feat: first pass, editing artifact

* feat: first pass, robust artifact replacement

* fix: robust artifact replacement & re-render when expected

* feat: Download Artifacts

* refactor: improve artifact editing UX

* fix: layout shift of new download button

* fix: enhance missing output checks and logging in StreamRunManager
2025-01-23 18:19:04 -05:00

151 lines
3.4 KiB
TypeScript

import debounce from 'lodash/debounce';
import React, { memo, useEffect, useMemo, useCallback } from 'react';
import {
useSandpack,
SandpackCodeEditor,
SandpackProvider as StyledProvider,
} from '@codesandbox/sandpack-react';
import { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
import type { ArtifactFiles, Artifact } from '~/common';
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
import { useEditArtifact } from '~/data-provider';
import { useEditorContext } from '~/Providers';
const createDebouncedMutation = (
callback: (params: {
index: number;
messageId: string;
original: string;
updated: string;
}) => void,
) => debounce(callback, 500);
const CodeEditor = ({
fileKey,
readOnly,
artifact,
editorRef,
}: {
fileKey: string;
readOnly: boolean;
artifact: Artifact;
editorRef: React.MutableRefObject<CodeEditorRef>;
}) => {
const { sandpack } = useSandpack();
const { isMutating, setIsMutating, setCurrentCode } = useEditorContext();
const editArtifact = useEditArtifact({
onMutate: () => {
setIsMutating(true);
},
onSuccess: () => {
setIsMutating(false);
},
onError: () => {
setIsMutating(false);
setCurrentCode(artifact.content);
},
});
const mutationCallback = useCallback(
(params: { index: number; messageId: string; original: string; updated: string }) => {
editArtifact.mutate(params);
},
[editArtifact],
);
const debouncedMutation = useMemo(
() => createDebouncedMutation(mutationCallback),
[mutationCallback],
);
useEffect(() => {
if (readOnly) {
return;
}
if (isMutating) {
return;
}
const currentCode = sandpack.files['/' + fileKey].code;
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
setCurrentCode(currentCode);
debouncedMutation({
index: artifact.index,
messageId: artifact.messageId ?? '',
original: artifact.content,
updated: currentCode,
});
}
return () => {
debouncedMutation.cancel();
};
}, [
fileKey,
artifact.index,
artifact.content,
artifact.messageId,
readOnly,
isMutating,
sandpack.files,
setIsMutating,
setCurrentCode,
debouncedMutation,
]);
return (
<SandpackCodeEditor
ref={editorRef}
showTabs={false}
readOnly={readOnly}
showRunButton={false}
showLineNumbers={true}
showInlineErrors={true}
className="hljs language-javascript bg-black"
/>
);
};
export const ArtifactCodeEditor = memo(function ({
files,
fileKey,
template,
artifact,
editorRef,
sharedProps,
isSubmitting,
}: {
fileKey: string;
artifact: Artifact;
files: ArtifactFiles;
isSubmitting: boolean;
template: SandpackProviderProps['template'];
sharedProps: Partial<SandpackProviderProps>;
editorRef: React.MutableRefObject<CodeEditorRef>;
}) {
if (Object.keys(files).length === 0) {
return null;
}
return (
<StyledProvider
theme="dark"
files={{
...files,
...sharedFiles,
}}
options={{ ...sharedOptions }}
{...sharedProps}
template={template}
>
<CodeEditor
editorRef={editorRef}
fileKey={fileKey}
readOnly={isSubmitting}
artifact={artifact}
/>
</StyledProvider>
);
});