feat: Artifact Management Enhancements, Version Control, and UI Refinements (#10318)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

*  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:
Danny Avila 2025-11-12 13:32:47 -05:00 committed by GitHub
parent 4186db3ce2
commit b8b1217c34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1565 additions and 345 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@librechat/client",
"version": "0.3.1",
"version": "0.3.2",
"description": "React components for LibreChat",
"main": "dist/index.js",
"module": "dist/index.es.js",
@ -64,7 +64,7 @@
"react-dom": "^18.2.0 || ^19.1.0",
"react-hook-form": "^7.56.4",
"react-i18next": "^15.4.0 || ^15.6.0",
"react-resizable-panels": "^3.0.2",
"react-resizable-panels": "^3.0.6",
"react-textarea-autosize": "^8.4.0",
"tailwind-merge": "^1.9.1"
},

View file

@ -53,13 +53,23 @@ export default function Badge({
}
};
const getWhileTapScale = () => {
if (isDragging) {
return 1.1;
}
if (isDisabled) {
return 1;
}
return 0.97;
};
return (
<motion.button
onClick={handleClick}
className={cn(
'group relative inline-flex items-center gap-1.5 rounded-full px-4 py-1.5',
'border border-border-medium text-sm font-medium transition-shadow md:w-full',
'size-9 p-2 md:p-3',
'border border-border-medium text-sm font-medium transition-shadow',
'@container-[600px]:w-full size-9 p-2',
isActive
? 'bg-surface-active shadow-md'
: 'bg-surface-chat shadow-sm hover:bg-surface-hover hover:shadow-md',
@ -72,16 +82,23 @@ export default function Badge({
scale: isDragging ? 1.1 : 1,
boxShadow: isDragging ? '0 10px 25px rgba(0,0,0,0.1)' : undefined,
}}
whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }}
whileTap={{ scale: getWhileTapScale() }}
transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }}
{...(props as React.ComponentProps<typeof motion.button>)}
>
{Icon && <Icon className={cn('relative h-5 w-5 md:h-4 md:w-4', !label && 'mx-auto')} />}
<span className="relative hidden md:inline">{label}</span>
{Icon && (
<Icon
className={cn(
'@container-[600px]:h-4 @container-[600px]:w-4 relative h-5 w-5',
!label && 'mx-auto',
)}
/>
)}
<span className="@container-[600px]:inline relative hidden">{label}</span>
{isEditing && !isDragging && (
<motion.button
className="absolute -right-1 -top-1 flex h-6 w-6 items-center justify-center rounded-full bg-surface-secondary-alt text-text-primary shadow-sm md:h-5 md:w-5"
className="@container-[600px]:h-5 @container-[600px]:w-5 absolute -right-1 -top-1 flex h-6 w-6 items-center justify-center rounded-full bg-surface-secondary-alt text-text-primary shadow-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}

View file

@ -4,6 +4,7 @@ import { useLocalize } from '~/hooks';
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface RadioProps {
@ -11,9 +12,18 @@ interface RadioProps {
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
className?: string;
fullWidth?: boolean;
}
const Radio = memo(function Radio({ options, value, onChange, disabled = false }: RadioProps) {
const Radio = memo(function Radio({
options,
value,
onChange,
disabled = false,
className = '',
fullWidth = false,
}: RadioProps) {
const localize = useLocalize();
const [currentValue, setCurrentValue] = useState<string>(value ?? '');
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
@ -67,7 +77,10 @@ const Radio = memo(function Radio({ options, value, onChange, disabled = false }
const selectedIndex = options.findIndex((opt) => opt.value === currentValue);
return (
<div className="relative inline-flex items-center rounded-lg bg-muted p-1" role="radiogroup">
<div
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
role="radiogroup"
>
{selectedIndex >= 0 && (
<div
className="pointer-events-none absolute inset-y-1 rounded-md border border-border/50 bg-background shadow-sm transition-all duration-300 ease-out"
@ -85,10 +98,11 @@ const Radio = memo(function Radio({ options, value, onChange, disabled = false }
aria-checked={currentValue === option.value}
onClick={() => handleChange(option.value)}
disabled={disabled}
className={`relative z-10 flex h-[34px] items-center justify-center rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
className={`relative z-10 flex h-[34px] items-center justify-center gap-2 rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
currentValue === option.value ? 'text-foreground' : 'text-foreground'
} ${disabled ? 'cursor-not-allowed opacity-50' : ''}`}
} ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''}`}
>
{option.icon && <span className="flex-shrink-0">{option.icon}</span>}
<span className="whitespace-nowrap">{option.label}</span>
</button>
))}

View file

@ -52,7 +52,7 @@ const ResizableHandleAlt = ({
{...props}
>
{withHandle && (
<div className="invisible z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border group-hover:visible group-active:visible">
<div className="invisible z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border group-hover:visible group-active:visible group-data-[resize-handle-active]:visible">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}

View file

@ -64,7 +64,7 @@ export const messages = (params: q.MessagesListParams) => {
return `${messagesRoot}${buildQuery(rest)}`;
};
export const messagesArtifacts = (messageId: string) => `${messagesRoot}/artifacts/${messageId}`;
export const messagesArtifacts = (messageId: string) => `${messagesRoot}/artifact/${messageId}`;
const shareRoot = `${BASE_URL}/api/share`;
export const shareMessages = (shareId: string) => `${shareRoot}/${shareId}`;