From 0ee5712df149878ea0d2e5f08761d8a8c80afbf6 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:41:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Enhance=20Artifact=20Manage?= =?UTF-8?q?ment=20with=20Version=20Control=20and=20UI=20Improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Artifacts/ArtifactButton.tsx | 12 +- .../Artifacts/ArtifactCodeEditor.tsx | 4 + .../components/Artifacts/ArtifactPreview.tsx | 25 +-- .../src/components/Artifacts/ArtifactTabs.tsx | 7 +- .../components/Artifacts/ArtifactVersion.tsx | 78 ++++++++++ client/src/components/Artifacts/Artifacts.tsx | 142 +++++++++--------- client/src/components/Artifacts/Code.tsx | 12 +- .../components/Artifacts/DownloadArtifact.tsx | 21 +-- client/src/hooks/Artifacts/useArtifacts.ts | 1 + client/src/locales/en/translation.json | 1 + 10 files changed, 194 insertions(+), 109 deletions(-) create mode 100644 client/src/components/Artifacts/ArtifactVersion.tsx diff --git a/client/src/components/Artifacts/ArtifactButton.tsx b/client/src/components/Artifacts/ArtifactButton.tsx index 162e7d717c..bfb9037210 100644 --- a/client/src/components/Artifacts/ArtifactButton.tsx +++ b/client/src/components/Artifacts/ArtifactButton.tsx @@ -13,8 +13,9 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => { const location = useLocation(); const setVisible = useSetRecoilState(store.artifactsVisibility); const [artifacts, setArtifacts] = useRecoilState(store.artifactsState); - const setCurrentArtifactId = useSetRecoilState(store.currentArtifactId); + const [currentArtifactId, setCurrentArtifactId] = useRecoilState(store.currentArtifactId); const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId); + const isSelected = artifact?.id === currentArtifactId; const [visibleArtifacts, setVisibleArtifacts] = useRecoilState(store.visibleArtifacts); const debouncedSetVisibleRef = useRef( @@ -69,9 +70,14 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => { setCurrentArtifactId(artifact.id); }, 15); }} - className="relative overflow-hidden rounded-xl border border-border-medium transition-all duration-300 hover:border-border-xheavy hover:shadow-lg" + className={ + `relative overflow-hidden rounded-xl transition-all duration-200 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg ` + + (isSelected + ? 'border-border-medium bg-surface-hover shadow-lg' + : 'border-border-light bg-surface-tertiary shadow-sm') + } > -
+
diff --git a/client/src/components/Artifacts/ArtifactCodeEditor.tsx b/client/src/components/Artifacts/ArtifactCodeEditor.tsx index f4f25280f9..43e13f5191 100644 --- a/client/src/components/Artifacts/ArtifactCodeEditor.tsx +++ b/client/src/components/Artifacts/ArtifactCodeEditor.tsx @@ -1,5 +1,6 @@ import debounce from 'lodash/debounce'; import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; import { useSandpack, SandpackCodeEditor, @@ -116,6 +117,9 @@ const CodeEditor = ({ showLineNumbers={true} showInlineErrors={true} readOnly={readOnly === true} + extensions={[autocompletion()]} + // @ts-ignore + extensionsKeymap={[completionKeymap]} className="hljs language-javascript bg-black" /> ); diff --git a/client/src/components/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx index 3764119f3a..ee7d22852f 100644 --- a/client/src/components/Artifacts/ArtifactPreview.tsx +++ b/client/src/components/Artifacts/ArtifactPreview.tsx @@ -1,10 +1,6 @@ -import React, { memo, useMemo } from 'react'; -import { - SandpackPreview, - SandpackProvider, - SandpackProviderProps, -} from '@codesandbox/sandpack-react/unstyled'; -import type { SandpackPreviewRef, PreviewProps } from '@codesandbox/sandpack-react/unstyled'; +import React, { memo, useMemo, type MutableRefObject } from 'react'; +import { SandpackPreview, SandpackProvider } from '@codesandbox/sandpack-react/unstyled'; +import type { SandpackProviderProps, SandpackPreviewRef, PreviewProps } from '@codesandbox/sandpack-react/unstyled'; import type { TStartupConfig } from 'librechat-data-provider'; import type { ArtifactFiles } from '~/common'; import { sharedFiles, sharedOptions } from '~/utils/artifacts'; @@ -22,7 +18,7 @@ export const ArtifactPreview = memo(function ({ fileKey: string; template: SandpackProviderProps['template']; sharedProps: Partial; - previewRef: React.MutableRefObject; + previewRef: MutableRefObject; currentCode?: string; startupConfig?: TStartupConfig; }) { @@ -36,9 +32,7 @@ export const ArtifactPreview = memo(function ({ } return { ...files, - [fileKey]: { - code, - }, + [fileKey]: { code }, }; }, [currentCode, files, fileKey]); @@ -46,12 +40,10 @@ export const ArtifactPreview = memo(function ({ if (!startupConfig) { return sharedOptions; } - const _options: typeof sharedOptions = { + return { ...sharedOptions, bundlerURL: template === 'static' ? startupConfig.staticBundlerURL : startupConfig.bundlerURL, }; - - return _options; }, [startupConfig, template]); if (Object.keys(artifactFiles).length === 0) { @@ -60,10 +52,7 @@ export const ArtifactPreview = memo(function ({ return ( ; previewRef: React.MutableRefObject; + isSubmitting: boolean; }) { const { isSubmitting } = useArtifactsContext(); const { currentCode, setCurrentCode } = useEditorContext(); const { data: startupConfig } = useGetStartupConfig(); const lastIdRef = useRef(null); + useEffect(() => { if (artifact.id !== lastIdRef.current) { setCurrentCode(undefined); @@ -33,7 +36,9 @@ export default function ArtifactTabs({ const content = artifact.content ?? ''; const contentRef = useRef(null); useAutoScroll({ ref: contentRef, content, isSubmitting }); + const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact }); + return ( <> void; +} + +export default function ArtifactVersion({ + currentIndex, + totalVersions, + onVersionChange, +}: ArtifactVersionProps) { + const localize = useLocalize(); + const [isPopoverActive, setIsPopoverActive] = useState(false); + const isSmallScreen = useMediaQuery('(max-width: 768px)'); + const menuId = 'version-dropdown-menu'; + + const handleValueChange = (value: string) => { + const index = parseInt(value, 10); + onVersionChange(index); + setIsPopoverActive(false); + }; + + if (totalVersions <= 1) { + return null; + } + + const options = Array.from({ length: totalVersions }, (_, index) => ({ + value: index.toString(), + label: localize('com_ui_version_var', { 0: String(index + 1) }), + })); + + const dropdownItems = options.map((option) => { + const isSelected = option.value === String(currentIndex); + return { + label: option.label, + onClick: () => handleValueChange(option.value), + value: option.value, + icon: isSelected ? ( +