mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-01 16:18:51 +01:00
* 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
151 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
});
|