mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-23 02:36:12 +01:00
🚀 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
This commit is contained in:
parent
87383fec27
commit
ed57bb4711
34 changed files with 1156 additions and 237 deletions
151
client/src/components/Artifacts/ArtifactCodeEditor.tsx
Normal file
151
client/src/components/Artifacts/ArtifactCodeEditor.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue