mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-15 20:26:33 +01:00
✨ feat: Artifact Management Enhancements, Version Control, and UI Refinements (#10318)
* ✨ feat: Enhance Artifact Management with Version Control and UI Improvements ✨ feat: Improve mobile layout and responsiveness in Artifacts component ✨ feat: Refactor imports and remove unnecessary props in Artifact components ✨ feat: Enhance Artifacts and SidePanel components with improved mobile responsiveness and layout transitions feat: Enhance artifact panel animations and improve UI responsiveness - Updated Thinking component button styles for smoother transitions. - Implemented dynamic rendering for artifacts panel with animation effects. - Refactored localization keys for consistency across multiple languages. - Added new CSS animations for iOS-inspired smooth transitions. - Improved Tailwind CSS configuration to support enhanced animation effects. ✨ feat: Add fullWidth and icon support to Radio component for enhanced flexibility refactor: Remove unused PreviewProps import in ArtifactPreview component refactor: Improve button class handling and blur effect constants in Artifact components ✨ feat: Refactor Artifacts component structure and add mobile/desktop variants for improved UI chore: Bump @librechat/client version to 0.3.2 refactor: Update button styles and transition durations for improved UI responsiveness refactor: revert back localization key refactor: remove unused scaling and animation properties for cleaner CSS refactor: remove unused animation properties for cleaner configuration * ✨ refactor: Simplify className usage in ArtifactTabs, ArtifactsHeader, and SidePanelGroup components * refactor: Remove cycleArtifact function from useArtifacts hook * ✨ feat: Implement Chromium resize lag fix with performance optimizations and new ArtifactsPanel component * ✨ feat: Update Badge component for responsive design and improve tap scaling behavior * chore: Update react-resizable-panels dependency to version 3.0.6 * ✨ feat: Refactor Artifacts components for improved structure and performance; remove unused files and optimize styles * ✨ style: Update text color for improved visibility in Artifacts component * ✨ style: Remove text color class for improved Spinner styling in Artifacts component * refactor: Split EditorContext into MutationContext and CodeContext to optimize re-renders; update related components to use new hooks * refactor: Optimize debounced mutation handling in CodeEditor component using refs to maintain current values and reduce re-renders * fix: Correct endpoint for message artifacts by changing URL segment from 'artifacts' to 'artifact' * feat: Enhance useEditArtifact mutation with optimistic updates and rollback on error; improve type safety with context management * fix: proper switch to preview as soon as artifact becomes enclosed * refactor: Remove optimistic updates from useEditArtifact mutation to prevent errors; simplify onMutate logic * test: Add comprehensive unit tests for useArtifacts hook to validate artifact handling, tab switching, and state management * test: Enhance unit tests for useArtifacts hook to cover new conversation transitions and null message handling --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
parent
4186db3ce2
commit
b8b1217c34
25 changed files with 1565 additions and 345 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import React, { useMemo, useState, useEffect, useRef, memo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { KeyBinding } from '@codemirror/view';
|
||||
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
||||
import {
|
||||
useSandpack,
|
||||
SandpackCodeEditor,
|
||||
|
|
@ -10,116 +12,143 @@ 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 { useEditorContext, useArtifactsContext } from '~/Providers';
|
||||
import { useMutationState, useCodeState } from '~/Providers/EditorContext';
|
||||
import { useArtifactsContext } from '~/Providers';
|
||||
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
||||
|
||||
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 [currentUpdate, setCurrentUpdate] = useState<string | null>(null);
|
||||
const { isMutating, setIsMutating, setCurrentCode } = useEditorContext();
|
||||
const editArtifact = useEditArtifact({
|
||||
onMutate: (vars) => {
|
||||
setIsMutating(true);
|
||||
setCurrentUpdate(vars.updated);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsMutating(false);
|
||||
setCurrentUpdate(null);
|
||||
},
|
||||
onError: () => {
|
||||
setIsMutating(false);
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
if (artifact.index == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
|
||||
const isNotOriginal =
|
||||
currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim();
|
||||
const isNotRepeated =
|
||||
currentUpdate == null
|
||||
? true
|
||||
: currentCode != null && currentCode.trim() !== currentUpdate.trim();
|
||||
|
||||
if (artifact.content && isNotOriginal && isNotRepeated) {
|
||||
setCurrentCode(currentCode);
|
||||
debouncedMutation({
|
||||
index: artifact.index,
|
||||
messageId: artifact.messageId ?? '',
|
||||
original: artifact.content,
|
||||
updated: currentCode,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
debouncedMutation.cancel();
|
||||
};
|
||||
}, [
|
||||
const CodeEditor = memo(
|
||||
({
|
||||
fileKey,
|
||||
artifact.index,
|
||||
artifact.content,
|
||||
artifact.messageId,
|
||||
readOnly,
|
||||
isMutating,
|
||||
currentUpdate,
|
||||
setIsMutating,
|
||||
sandpack.files,
|
||||
setCurrentCode,
|
||||
debouncedMutation,
|
||||
]);
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SandpackCodeEditor
|
||||
ref={editorRef}
|
||||
showTabs={false}
|
||||
showRunButton={false}
|
||||
showLineNumbers={true}
|
||||
showInlineErrors={true}
|
||||
readOnly={readOnly === true}
|
||||
className="hljs language-javascript bg-black"
|
||||
/>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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]);
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue