| | import React, { useMemo, useState, useEffect, useRef, memo } from 'react'; |
| | import debounce from 'lodash/debounce'; |
| | import { KeyBinding } from '@codemirror/view'; |
| | import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; |
| | import { |
| | useSandpack, |
| | SandpackCodeEditor, |
| | SandpackProvider as StyledProvider, |
| | } from '@codesandbox/sandpack-react'; |
| | import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled'; |
| | import type { SandpackBundlerFile } from '@codesandbox/sandpack-client'; |
| | import type { CodeEditorRef } from '@codesandbox/sandpack-react'; |
| | import type { ArtifactFiles, Artifact } from '~/common'; |
| | import { useEditArtifact, useGetStartupConfig } from '~/data-provider'; |
| | import { useMutationState, useCodeState } from '~/Providers/EditorContext'; |
| | import { useArtifactsContext } from '~/Providers'; |
| | import { sharedFiles, sharedOptions } from '~/utils/artifacts'; |
| |
|
| | 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); |
| | }, |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | const artifactRef = useRef(artifact); |
| | const isMutatingRef = useRef(isMutating); |
| | const currentUpdateRef = useRef(currentUpdate); |
| | const editArtifactRef = useRef(editArtifact); |
| | const setCurrentCodeRef = useRef(setCurrentCode); |
| |
|
| | useEffect(() => { |
| | artifactRef.current = artifact; |
| | }, [artifact]); |
| |
|
| | useEffect(() => { |
| | isMutatingRef.current = isMutating; |
| | }, [isMutating]); |
| |
|
| | useEffect(() => { |
| | currentUpdateRef.current = currentUpdate; |
| | }, [currentUpdate]); |
| |
|
| | useEffect(() => { |
| | editArtifactRef.current = editArtifact; |
| | }, [editArtifact]); |
| |
|
| | useEffect(() => { |
| | setCurrentCodeRef.current = setCurrentCode; |
| | }, [setCurrentCode]); |
| |
|
| | |
| | |
| | |
| | |
| | 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], |
| | ); |
| |
|
| | |
| | |
| | |
| | useEffect(() => { |
| | const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code; |
| | if (currentCode) { |
| | debouncedMutation(currentCode); |
| | } |
| | }, [sandpack.files, fileKey, debouncedMutation]); |
| |
|
| | |
| | |
| | |
| | useEffect(() => { |
| | return () => { |
| | debouncedMutation.cancel(); |
| | }; |
| | }, [artifact.id, debouncedMutation]); |
| |
|
| | 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" |
| | /> |
| | ); |
| | }, |
| | ); |
| |
|
| | export const ArtifactCodeEditor = function ({ |
| | files, |
| | fileKey, |
| | template, |
| | artifact, |
| | editorRef, |
| | sharedProps, |
| | readOnly: externalReadOnly, |
| | }: { |
| | fileKey: string; |
| | artifact: Artifact; |
| | files: ArtifactFiles; |
| | template: SandpackProviderProps['template']; |
| | sharedProps: Partial<SandpackProviderProps>; |
| | editorRef: React.MutableRefObject<CodeEditorRef>; |
| | readOnly?: boolean; |
| | }) { |
| | const { data: config } = useGetStartupConfig(); |
| | const { isSubmitting } = useArtifactsContext(); |
| | const options: typeof sharedOptions = useMemo(() => { |
| | if (!config) { |
| | return sharedOptions; |
| | } |
| | return { |
| | ...sharedOptions, |
| | activeFile: '/' + fileKey, |
| | bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL, |
| | }; |
| | }, [config, template, fileKey]); |
| | const initialReadOnly = (externalReadOnly ?? false) || (isSubmitting ?? false); |
| | const [readOnly, setReadOnly] = useState(initialReadOnly); |
| | useEffect(() => { |
| | setReadOnly((externalReadOnly ?? false) || (isSubmitting ?? false)); |
| | }, [isSubmitting, externalReadOnly]); |
| |
|
| | if (Object.keys(files).length === 0) { |
| | return null; |
| | } |
| |
|
| | return ( |
| | <StyledProvider |
| | theme="dark" |
| | files={{ |
| | ...files, |
| | ...sharedFiles, |
| | }} |
| | options={options} |
| | {...sharedProps} |
| | template={template} |
| | > |
| | <CodeEditor fileKey={fileKey} artifact={artifact} editorRef={editorRef} readOnly={readOnly} /> |
| | </StyledProvider> |
| | ); |
| | }; |
| |
|