2025-11-12 13:32:47 -05:00
|
|
|
import React, { useMemo, useState, useEffect, useRef, memo } from 'react';
|
2025-01-23 18:19:04 -05:00
|
|
|
import debounce from 'lodash/debounce';
|
2025-11-12 13:32:47 -05:00
|
|
|
import { KeyBinding } from '@codemirror/view';
|
|
|
|
|
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
2025-01-23 18:19:04 -05:00
|
|
|
import {
|
|
|
|
|
useSandpack,
|
|
|
|
|
SandpackCodeEditor,
|
|
|
|
|
SandpackProvider as StyledProvider,
|
|
|
|
|
} from '@codesandbox/sandpack-react';
|
2025-04-06 03:28:05 -04:00
|
|
|
import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
|
|
|
|
|
import type { SandpackBundlerFile } from '@codesandbox/sandpack-client';
|
2025-01-23 18:19:04 -05:00
|
|
|
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
|
|
|
|
|
import type { ArtifactFiles, Artifact } from '~/common';
|
2025-03-05 16:03:54 -05:00
|
|
|
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
2025-11-12 13:32:47 -05:00
|
|
|
import { useMutationState, useCodeState } from '~/Providers/EditorContext';
|
|
|
|
|
import { useArtifactsContext } from '~/Providers';
|
2025-01-23 18:19:04 -05:00
|
|
|
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
|
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
const CodeEditor = memo(
|
|
|
|
|
({
|
|
|
|
|
fileKey,
|
|
|
|
|
readOnly,
|
|
|
|
|
artifact,
|
|
|
|
|
editorRef,
|
|
|
|
|
}: {
|
|
|
|
|
fileKey: string;
|
|
|
|
|
readOnly?: boolean;
|
|
|
|
|
artifact: Artifact;
|
|
|
|
|
editorRef: React.MutableRefObject<CodeEditorRef>;
|
|
|
|
|
}) => {
|
|
|
|
|
const { sandpack } = useSandpack();
|
|
|
|
|
const [currentUpdate, setCurrentUpdate] = useState<string | null>(null);
|
|
|
|
|
const { isMutating, setIsMutating } = useMutationState();
|
|
|
|
|
const { setCurrentCode } = useCodeState();
|
|
|
|
|
const editArtifact = useEditArtifact({
|
|
|
|
|
onMutate: (vars) => {
|
|
|
|
|
setIsMutating(true);
|
|
|
|
|
setCurrentUpdate(vars.updated);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setIsMutating(false);
|
|
|
|
|
setCurrentUpdate(null);
|
|
|
|
|
},
|
|
|
|
|
onError: () => {
|
|
|
|
|
setIsMutating(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
/**
|
|
|
|
|
* Create stable debounced mutation that doesn't depend on changing callbacks
|
|
|
|
|
* Use refs to always access the latest values without recreating the debounce
|
|
|
|
|
*/
|
|
|
|
|
const artifactRef = useRef(artifact);
|
|
|
|
|
const isMutatingRef = useRef(isMutating);
|
|
|
|
|
const currentUpdateRef = useRef(currentUpdate);
|
|
|
|
|
const editArtifactRef = useRef(editArtifact);
|
|
|
|
|
const setCurrentCodeRef = useRef(setCurrentCode);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
artifactRef.current = artifact;
|
|
|
|
|
}, [artifact]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
isMutatingRef.current = isMutating;
|
|
|
|
|
}, [isMutating]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
currentUpdateRef.current = currentUpdate;
|
|
|
|
|
}, [currentUpdate]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
editArtifactRef.current = editArtifact;
|
|
|
|
|
}, [editArtifact]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
setCurrentCodeRef.current = setCurrentCode;
|
|
|
|
|
}, [setCurrentCode]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
/**
|
|
|
|
|
* Create debounced mutation once - never recreate it
|
|
|
|
|
* All values are accessed via refs so they're always current
|
|
|
|
|
*/
|
|
|
|
|
const debouncedMutation = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
debounce((code: string) => {
|
|
|
|
|
if (readOnly) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isMutatingRef.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (artifactRef.current.index == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const artifact = artifactRef.current;
|
|
|
|
|
const artifactIndex = artifact.index;
|
|
|
|
|
const isNotOriginal =
|
|
|
|
|
code && artifact.content != null && code.trim() !== artifact.content.trim();
|
|
|
|
|
const isNotRepeated =
|
|
|
|
|
currentUpdateRef.current == null
|
|
|
|
|
? true
|
|
|
|
|
: code != null && code.trim() !== currentUpdateRef.current.trim();
|
|
|
|
|
|
|
|
|
|
if (artifact.content && isNotOriginal && isNotRepeated && artifactIndex != null) {
|
|
|
|
|
setCurrentCodeRef.current(code);
|
|
|
|
|
editArtifactRef.current.mutate({
|
|
|
|
|
index: artifactIndex,
|
|
|
|
|
messageId: artifact.messageId ?? '',
|
|
|
|
|
original: artifact.content,
|
|
|
|
|
updated: code,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 500),
|
|
|
|
|
[readOnly],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Listen to Sandpack file changes and trigger debounced mutation
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
|
|
|
|
|
if (currentCode) {
|
|
|
|
|
debouncedMutation(currentCode);
|
|
|
|
|
}
|
|
|
|
|
}, [sandpack.files, fileKey, debouncedMutation]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cleanup: cancel pending mutations when component unmounts or artifact changes
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
debouncedMutation.cancel();
|
|
|
|
|
};
|
|
|
|
|
}, [artifact.id, debouncedMutation]);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-11-12 13:32:47 -05:00
|
|
|
return (
|
|
|
|
|
<SandpackCodeEditor
|
|
|
|
|
ref={editorRef}
|
|
|
|
|
showTabs={false}
|
|
|
|
|
showRunButton={false}
|
|
|
|
|
showLineNumbers={true}
|
|
|
|
|
showInlineErrors={true}
|
|
|
|
|
readOnly={readOnly === true}
|
|
|
|
|
extensions={[autocompletion()]}
|
|
|
|
|
extensionsKeymap={Array.from<KeyBinding>(completionKeymap)}
|
|
|
|
|
className="hljs language-javascript bg-black"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-01-23 18:19:04 -05:00
|
|
|
|
2025-08-08 22:44:58 -04:00
|
|
|
export const ArtifactCodeEditor = function ({
|
2025-01-23 18:19:04 -05:00
|
|
|
files,
|
|
|
|
|
fileKey,
|
|
|
|
|
template,
|
|
|
|
|
artifact,
|
|
|
|
|
editorRef,
|
|
|
|
|
sharedProps,
|
|
|
|
|
}: {
|
|
|
|
|
fileKey: string;
|
|
|
|
|
artifact: Artifact;
|
|
|
|
|
files: ArtifactFiles;
|
|
|
|
|
template: SandpackProviderProps['template'];
|
|
|
|
|
sharedProps: Partial<SandpackProviderProps>;
|
|
|
|
|
editorRef: React.MutableRefObject<CodeEditorRef>;
|
|
|
|
|
}) {
|
2025-03-05 16:03:54 -05:00
|
|
|
const { data: config } = useGetStartupConfig();
|
2025-08-08 22:44:58 -04:00
|
|
|
const { isSubmitting } = useArtifactsContext();
|
2025-03-05 16:03:54 -05:00
|
|
|
const options: typeof sharedOptions = useMemo(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return sharedOptions;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...sharedOptions,
|
2025-10-11 23:37:35 +09:00
|
|
|
activeFile: '/' + fileKey,
|
2025-04-10 15:37:23 -04:00
|
|
|
bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL,
|
2025-03-05 16:03:54 -05:00
|
|
|
};
|
2025-10-11 23:37:35 +09:00
|
|
|
}, [config, template, fileKey]);
|
2025-08-08 22:44:58 -04:00
|
|
|
const [readOnly, setReadOnly] = useState(isSubmitting ?? false);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setReadOnly(isSubmitting ?? false);
|
|
|
|
|
}, [isSubmitting]);
|
2025-03-05 16:03:54 -05:00
|
|
|
|
2025-01-23 18:19:04 -05:00
|
|
|
if (Object.keys(files).length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StyledProvider
|
|
|
|
|
theme="dark"
|
|
|
|
|
files={{
|
|
|
|
|
...files,
|
|
|
|
|
...sharedFiles,
|
|
|
|
|
}}
|
2025-03-05 16:03:54 -05:00
|
|
|
options={options}
|
2025-01-23 18:19:04 -05:00
|
|
|
{...sharedProps}
|
|
|
|
|
template={template}
|
|
|
|
|
>
|
2025-08-08 22:44:58 -04:00
|
|
|
<CodeEditor fileKey={fileKey} artifact={artifact} editorRef={editorRef} readOnly={readOnly} />
|
2025-01-23 18:19:04 -05:00
|
|
|
</StyledProvider>
|
|
|
|
|
);
|
2025-08-08 22:44:58 -04:00
|
|
|
};
|