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.
This commit is contained in:
Marco Beretta 2025-10-24 11:08:57 +02:00
parent 44fa479bd4
commit 7e1d02bcc3
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
35 changed files with 416 additions and 104 deletions

View file

@ -55,40 +55,52 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
return (
<div className="group relative my-4 rounded-xl text-sm text-text-primary">
<button
type="button"
onClick={() => {
if (!location.pathname.includes('/c/')) {
{(() => {
const handleClick = () => {
if (!location.pathname.includes('/c/')) return;
if (isSelected) {
resetCurrentArtifactId();
setVisible(false);
return;
}
resetCurrentArtifactId();
setVisible(true);
if (artifacts?.[artifact.id] == null) {
setArtifacts(visibleArtifacts);
}
setTimeout(() => {
setCurrentArtifactId(artifact.id);
}, 15);
}}
className={
`relative overflow-hidden rounded-xl transition-all duration-200 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg ` +
};
const buttonClass =
`relative overflow-hidden rounded-xl transition-all duration-300 hover:border-border-medium hover:bg-surface-hover hover:shadow-lg active:scale-[0.98] ` +
(isSelected
? 'border-border-medium bg-surface-hover shadow-lg'
: 'border-border-light bg-surface-tertiary shadow-sm')
}
>
<div className="w-fit p-2">
<div className="flex flex-row items-center gap-2">
<FilePreview fileType={fileType} className="relative" />
<div className="overflow-hidden text-left">
<div className="truncate font-medium">{artifact.title}</div>
<div className="truncate text-text-secondary">
{localize('com_ui_artifact_click')}
: 'border-border-light bg-surface-tertiary shadow-sm');
const actionLabel = isSelected
? localize('com_ui_click_to_close')
: localize('com_ui_click_to_open');
return (
<button type="button" onClick={handleClick} className={buttonClass}>
<div className="w-fit p-2">
<div className="flex flex-row items-center gap-2">
<FilePreview fileType={fileType} className="relative" />
<div className="overflow-hidden text-left">
<div className="truncate font-medium">{artifact.title}</div>
<div className="truncate text-text-secondary">{actionLabel}</div>
</div>
</div>
</div>
</div>
</div>
</button>
</button>
);
})()}
<br />
</div>
);

View file

@ -1,6 +1,10 @@
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 {
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';

View file

@ -39,12 +39,16 @@ export default function ArtifactTabs({
const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact });
return (
<>
<div className="flex h-full w-full flex-col">
<Tabs.Content
ref={contentRef}
value="code"
id="artifacts-code"
className={cn('flex-grow overflow-auto')}
className={cn(
'h-full w-full flex-grow overflow-auto',
'data-[state=active]:duration-200 data-[state=active]:animate-in data-[state=active]:fade-in-0',
'data-[state=inactive]:duration-150 data-[state=inactive]:animate-out data-[state=inactive]:fade-out-0',
)}
tabIndex={-1}
>
<ArtifactCodeEditor
@ -56,7 +60,16 @@ export default function ArtifactTabs({
sharedProps={sharedProps}
/>
</Tabs.Content>
<Tabs.Content value="preview" className="flex-grow overflow-auto" tabIndex={-1}>
<Tabs.Content
value="preview"
className={cn(
'h-full w-full flex-grow overflow-auto',
'data-[state=active]:duration-200 data-[state=active]:animate-in data-[state=active]:fade-in-0',
'data-[state=inactive]:duration-150 data-[state=inactive]:animate-out data-[state=inactive]:fade-out-0',
)}
tabIndex={-1}
>
<ArtifactPreview
files={files}
fileKey={fileKey}
@ -67,6 +80,6 @@ export default function ArtifactTabs({
startupConfig={startupConfig}
/>
</Tabs.Content>
</>
</div>
);
}

View file

@ -50,6 +50,7 @@ export default function ArtifactVersion({
return (
<DropdownPopup
menuId={menuId}
portal
focusLoop
unmountOnHide
isOpen={isPopoverActive}

View file

@ -1,7 +1,7 @@
import { useRef, useState, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { Code, Play, RefreshCw, X } from 'lucide-react';
import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { Button, Spinner, useMediaQuery } from '@librechat/client';
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
@ -21,12 +21,49 @@ export default function Artifacts() {
const editorRef = useRef<CodeEditorRef>();
const previewRef = useRef<SandpackPreviewRef>();
const [isVisible, setIsVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [height, setHeight] = useState(90); // Height as percentage of viewport
const [isDragging, setIsDragging] = useState(false);
const [blurAmount, setBlurAmount] = useState(0); // Dynamic blur amount
const dragStartY = useRef(0);
const dragStartHeight = useRef(90);
const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility);
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
useEffect(() => {
setIsVisible(true);
}, []);
setIsMounted(true);
const delay = isMobile ? 50 : 30;
const timer = setTimeout(() => setIsVisible(true), delay);
return () => {
clearTimeout(timer);
setIsMounted(false);
};
}, [isMobile]);
// Dynamic blur based on height - more blur when taking up more screen
useEffect(() => {
if (!isMobile) {
setBlurAmount(0);
return;
}
// Calculate blur amount based on how much screen is covered
// 50% height = no blur, 100% height = full blur
const minHeightForBlur = 50;
const maxHeightForBlur = 100;
if (height <= minHeightForBlur) {
setBlurAmount(0);
} else if (height >= maxHeightForBlur) {
setBlurAmount(32); // Increased from 16 to 32 for stronger blur
} else {
// Linear interpolation between 0 and 32px blur
const progress = (height - minHeightForBlur) / (maxHeightForBlur - minHeightForBlur);
setBlurAmount(Math.round(progress * 32)); // Changed from 16 to 32
}
}, [height, isMobile]);
const {
activeTab,
@ -37,7 +74,47 @@ export default function Artifacts() {
setCurrentArtifactId,
} = useArtifacts();
if (!currentArtifact) {
const handleDragStart = (e: React.PointerEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handleDragMove = (e: React.PointerEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const viewportHeight = window.innerHeight;
const deltaPercentage = (deltaY / viewportHeight) * 100;
const newHeight = Math.max(10, Math.min(100, dragStartHeight.current + deltaPercentage));
setHeight(newHeight);
};
const handleDragEnd = (e: React.PointerEvent) => {
if (!isDragging) return;
setIsDragging(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
// Snap to positions based on final height
if (height < 30) {
// Close if dragged down significantly
closeArtifacts();
} else if (height > 95) {
// Snap to full height if dragged near top
setHeight(100);
} else if (height < 60) {
// Snap to minimum if in lower range
setHeight(50);
} else {
// Snap to default
setHeight(90);
}
};
if (!currentArtifact || !isMounted) {
return null;
}
@ -51,57 +128,97 @@ export default function Artifacts() {
};
const closeArtifacts = () => {
setIsVisible(false);
setTimeout(() => setArtifactsVisible(false), isMobile ? 400 : 500);
if (isMobile) {
setIsClosing(true);
setIsVisible(false);
setTimeout(() => {
setArtifactsVisible(false);
setIsClosing(false);
setHeight(90); // Reset height
}, 250);
} else {
resetCurrentArtifactId();
setArtifactsVisible(false);
}
};
// Calculate backdrop opacity based on blur amount
const backdropOpacity = blurAmount > 0 ? Math.min(0.3, blurAmount / 53.33) : 0;
return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
<div className="flex h-full w-full flex-col">
{/* Mobile backdrop */}
{/* Mobile backdrop with dynamic blur */}
{isMobile && (
<div
className={cn(
'duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)] fixed inset-0 z-[99] bg-black/40 backdrop-blur-md transition-opacity',
isVisible ? 'opacity-100' : 'pointer-events-none opacity-0',
'fixed inset-0 z-[99] bg-black will-change-[opacity,backdrop-filter]',
isVisible && !isClosing
? 'transition-all duration-300'
: 'pointer-events-none opacity-0 backdrop-blur-none transition-opacity duration-150',
// Allow pointer events when not fully blurred so chat is scrollable
blurAmount < 8 && isVisible && !isClosing ? 'pointer-events-none' : '',
)}
onClick={closeArtifacts}
style={{
opacity: isVisible && !isClosing ? backdropOpacity : 0,
backdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
WebkitBackdropFilter: isVisible && !isClosing ? `blur(${blurAmount}px)` : 'none',
}}
onClick={blurAmount >= 8 ? closeArtifacts : undefined}
aria-hidden="true"
/>
)}
<div
className={cn(
'flex h-full w-full flex-col overflow-hidden bg-surface-primary text-xl text-text-primary',
'flex w-full flex-col bg-surface-primary text-xl text-text-primary',
isMobile
? cn(
'duration-400 ease-[cubic-bezier(0.25,0.46,0.45,0.94)] fixed inset-x-0 bottom-0 z-[100] h-[90vh] rounded-t-[20px] shadow-[0_-10px_40px_rgba(0,0,0,0.3)] transition-transform will-change-transform',
isVisible ? 'translate-y-0' : 'translate-y-full',
'fixed inset-x-0 bottom-0 z-[100] rounded-t-[20px] shadow-[0_-10px_60px_rgba(0,0,0,0.35)]',
isVisible && !isClosing
? 'translate-y-0 opacity-100'
: 'duration-250 translate-y-full opacity-0 transition-all',
isDragging ? '' : 'transition-all duration-300',
)
: cn(
'ease-[cubic-bezier(0.25,0.46,0.45,0.94)] shadow-xl transition-all duration-500 will-change-transform',
isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0',
'h-full shadow-2xl',
isVisible && !isClosing
? 'duration-350 translate-x-0 opacity-100 transition-all'
: 'translate-x-5 opacity-0 transition-all duration-300',
),
)}
style={isMobile ? { height: `${height}vh` } : { overflow: 'hidden' }}
>
{/* Mobile drag indicator */}
{isMobile && (
<div className="flex flex-shrink-0 items-center justify-center pb-1.5 pt-2">
<div className="h-1 w-10 rounded-full bg-border-medium opacity-50" />
<div
className="flex flex-shrink-0 cursor-grab items-center justify-center bg-surface-primary-alt pb-1.5 pt-2.5 active:cursor-grabbing"
onPointerDown={handleDragStart}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
onPointerCancel={handleDragEnd}
>
<div className="h-1 w-12 rounded-full bg-border-xheavy opacity-40 transition-all duration-200 active:opacity-60" />
</div>
)}
{/* Header */}
<div
className={cn(
'flex flex-shrink-0 items-center justify-between gap-2 overflow-x-auto border-b border-border-light bg-surface-primary-alt px-3 py-2',
isMobile && 'justify-center',
'flex flex-shrink-0 items-center justify-between gap-2 border-b border-border-light bg-surface-primary-alt px-3 py-2 transition-all duration-300',
isMobile ? 'justify-center' : 'overflow-hidden',
)}
>
{!isMobile && (
<div className="flex items-center">
<div
className={cn(
'flex items-center transition-all duration-500',
isVisible && !isClosing
? 'translate-x-0 opacity-100'
: '-translate-x-2 opacity-0',
)}
>
<Tabs.List className="relative inline-flex h-9 gap-2 rounded-xl bg-surface-tertiary p-0.5">
<div
className={cn(
'absolute top-0.5 h-8 rounded-xl bg-surface-primary-alt transition-transform duration-200 ease-out',
'duration-[350ms] absolute top-0.5 h-8 rounded-[10px] bg-surface-primary-alt shadow-sm transition-all will-change-transform',
activeTab === 'code'
? 'w-[42%] translate-x-0'
: 'w-[50%] translate-x-[calc(100%-0.250rem)]',
@ -109,24 +226,30 @@ export default function Artifacts() {
/>
<Tabs.Trigger
value="code"
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
className="relative z-10 flex items-center gap-1.5 rounded-[10px] border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 hover:text-text-primary active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
>
<Code className="size-3" />
<span>{localize('com_ui_code')}</span>
<Code className="size-3 transition-transform duration-200 group-active:scale-90" />
<span className="whitespace-nowrap">{localize('com_ui_code')}</span>
</Tabs.Trigger>
<Tabs.Trigger
value="preview"
disabled={isMutating}
className="relative z-10 flex items-center gap-1.5 rounded-xl border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 ease-out hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
className="relative z-10 flex items-center gap-1.5 rounded-[10px] border-transparent px-3 py-1 text-xs font-medium transition-all duration-200 hover:text-text-primary active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
>
<Play className="size-3" />
<span>{localize('com_ui_preview')}</span>
<Play className="size-3 transition-transform duration-200 group-active:scale-90" />
<span className="whitespace-nowrap">{localize('com_ui_preview')}</span>
</Tabs.Trigger>
</Tabs.List>
</div>
)}
<div className="flex min-w-max items-center gap-2">
<div
className={cn(
'flex items-center gap-2 transition-all duration-500',
isMobile ? 'min-w-max' : '',
isVisible && !isClosing ? 'translate-x-0 opacity-100' : 'translate-x-2 opacity-0',
)}
>
{activeTab === 'preview' && (
<Button
size="icon"
@ -134,9 +257,13 @@ export default function Artifacts() {
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={localize('com_ui_refresh')}
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
className="transition-all duration-150 active:scale-90"
>
{isRefreshing ? <Spinner size={16} /> : <RefreshCw size={16} />}
{isRefreshing ? (
<Spinner size={16} />
) : (
<RefreshCw size={16} className="transition-transform duration-200" />
)}
</Button>
)}
{activeTab !== 'preview' && isMutating && (
@ -161,35 +288,37 @@ export default function Artifacts() {
variant="ghost"
onClick={closeArtifacts}
aria-label={localize('com_ui_close')}
className="h-8 w-8 transition-transform duration-150 ease-out hover:scale-105 active:scale-95"
className="h-8 w-8 transition-all duration-150 hover:scale-105 active:scale-90"
>
<X size={16} />
</Button>
</div>
</div>
{/* Content Area - This is the key fix */}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<ArtifactTabs
artifact={currentArtifact}
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
/>
{/* Content Area - Fixed positioning to prevent layout shifts */}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden bg-surface-primary">
<div className="absolute inset-0 flex flex-col">
<ArtifactTabs
artifact={currentArtifact}
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
/>
</div>
</div>
{/* Mobile Tab Switcher */}
{/* Mobile Tab Switcher with iOS-style animation */}
{isMobile && (
<div className="pb-safe-offset-3 flex-shrink-0 border-t border-border-light bg-surface-primary-alt px-3 pt-2">
<Tabs.List className="relative flex h-10 w-full rounded-xl bg-surface-tertiary p-1">
<div
className={cn(
'duration-[250ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] absolute left-1 top-1 h-8 w-[calc(50%-0.25rem)] rounded-lg bg-surface-primary-alt shadow-sm transition-transform',
'duration-[350ms] absolute left-1 top-1 h-8 w-[calc(50%-0.25rem)] rounded-[10px] bg-surface-primary-alt shadow-sm transition-transform will-change-transform',
activeTab === 'code' ? 'translate-x-0' : 'translate-x-[calc(100%+0.5rem)]',
)}
/>
<Tabs.Trigger
value="code"
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-lg border-transparent py-1.5 text-xs font-medium transition-all duration-150 ease-out active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-[10px] border-transparent py-1.5 text-xs font-medium transition-all duration-150 active:scale-95 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
>
<Code className="size-3.5" />
<span>{localize('com_ui_code')}</span>
@ -197,7 +326,7 @@ export default function Artifacts() {
<Tabs.Trigger
value="preview"
disabled={isMutating}
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-lg border-transparent py-1.5 text-xs font-medium transition-all duration-150 ease-out active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
className="relative z-10 flex w-1/2 items-center justify-center gap-1.5 rounded-[10px] border-transparent py-1.5 text-xs font-medium transition-all duration-150 active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 data-[state=active]:text-text-primary data-[state=inactive]:text-text-secondary"
>
<Play className="size-3.5" />
<span>{localize('com_ui_preview')}</span>

View file

@ -7,8 +7,8 @@ import { cn } from '~/utils';
import store from '~/store';
const BUTTON_STYLES = {
base: 'group mt-3 flex w-fit items-center justify-center rounded-xl bg-surface-tertiary px-3 py-2 text-xs leading-[18px] animate-thinking-appear',
icon: 'icon-sm ml-1.5 transform-gpu text-text-primary transition-transform duration-200',
base: '-sring group mt-3 flex w-fit items-center justify-center rounded-xl bg-surface-tertiary px-3 py-2 text-xs leading-[18px] transition-all duration-300 hover:bg-surface-secondary active:scale-95 animate-thinking-appear',
icon: 'icon-sm ml-1.5 transform-gpu text-text-primary transition-transform duration-300',
} as const;
const CONTENT_STYLES = {
@ -69,7 +69,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
<ThinkingButton isExpanded={isExpanded} onClick={handleClick} label={label} />
</div>
<div
className={cn('grid transition-all duration-300 ease-out', isExpanded && 'mb-8')}
className={cn('duration-400 grid transition-all', isExpanded && 'mb-8')}
style={{
gridTemplateRows: isExpanded ? '1fr' : '0fr',
}}

View file

@ -14,6 +14,8 @@ import { normalizeLayout } from '~/utils';
import SidePanel from './SidePanel';
import store from '~/store';
const ANIMATION_DURATION = 500;
interface SidePanelProps {
defaultLayout?: number[] | undefined;
defaultCollapsed?: boolean;
@ -42,14 +44,43 @@ const SidePanelGroup = memo(
);
const panelRef = useRef<ImperativePanelHandle>(null);
const artifactsPanelRef = useRef<ImperativePanelHandle>(null);
const [minSize, setMinSize] = useState(defaultMinSize);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null);
const artifactsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const hideSidePanel = useRecoilValue(store.hideSidePanel);
useEffect(() => {
if (artifacts != null) {
if (artifactsTimeoutRef.current) {
clearTimeout(artifactsTimeoutRef.current);
artifactsTimeoutRef.current = null;
}
setShouldRenderArtifacts(true);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
artifactsPanelRef.current?.expand();
});
});
} else if (shouldRenderArtifacts) {
artifactsPanelRef.current?.collapse();
artifactsTimeoutRef.current = setTimeout(() => {
setShouldRenderArtifacts(false);
}, ANIMATION_DURATION);
}
return () => {
if (artifactsTimeoutRef.current) {
clearTimeout(artifactsTimeoutRef.current);
}
};
}, [artifacts, shouldRenderArtifacts]);
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
@ -120,20 +151,25 @@ const SidePanelGroup = memo(
>
{children}
</ResizablePanel>
{artifacts != null && !isSmallScreen && (
{shouldRenderArtifacts && !isSmallScreen && (
<>
<ResizableHandleAlt
withHandle
className="ml-3 bg-border-medium text-text-primary transition-opacity duration-300"
/>
{artifacts != null && (
<ResizableHandleAlt
withHandle
className="ml-3 bg-border-medium text-text-primary"
/>
)}
<ResizablePanel
defaultSize={currentLayout[1]}
ref={artifactsPanelRef}
defaultSize={artifacts != null ? currentLayout[1] : 0}
minSize={minSizeMain}
collapsible={true}
collapsedSize={0}
order={2}
id="artifacts-panel"
className="ease-[cubic-bezier(0.25,0.46,0.45,0.94)] transition-all duration-500"
>
{artifacts}
<div className="h-full min-w-[400px] overflow-hidden">{artifacts}</div>
</ResizablePanel>
</>
)}

View file

@ -427,7 +427,7 @@
"com_ui_all_proper": "الكل",
"com_ui_archive": "أرشفة",
"com_ui_archive_error": "فشل في أرشفة المحادثة",
"com_ui_artifact_click": "انقر للفتح",
"com_ui_click_to_open": "انقر للفتح",
"com_ui_artifacts": "المخرجات",
"com_ui_artifacts_toggle": "تبديل واجهة العناصر",
"com_ui_ascending": "تصاعدي",

View file

@ -508,7 +508,7 @@
"com_ui_archive": "Arxiva",
"com_ui_archive_delete_error": "No s'ha pogut eliminar la conversa arxivada",
"com_ui_archive_error": "No s'ha pogut arxivar la conversa",
"com_ui_artifact_click": "Fes clic per obrir",
"com_ui_click_to_open": "Fes clic per obrir",
"com_ui_artifacts": "Artifacts",
"com_ui_artifacts_toggle": "Activa/desactiva la UI d'artifacts",
"com_ui_artifacts_toggle_agent": "Habilita artifacts",

View file

@ -385,7 +385,7 @@
"com_ui_api_key": "API klíč",
"com_ui_archive": "Archivovat",
"com_ui_archive_error": "Nepodařilo se archivovat konverzaci",
"com_ui_artifact_click": "Klikněte pro otevření",
"com_ui_click_to_open": "Klikněte pro otevření",
"com_ui_artifacts": "Artefakty",
"com_ui_artifacts_toggle": "Přepnout uživatelské rozhraní artefaktů",
"com_ui_artifacts_toggle_agent": "Povolit artefakty",

View file

@ -531,7 +531,7 @@
"com_ui_archive": "Arkiv",
"com_ui_archive_delete_error": "Kunne ikke slette arkiveret samtale",
"com_ui_archive_error": "Kunne ikke arkivere samtale",
"com_ui_artifact_click": "Klik for at åbne",
"com_ui_click_to_open": "Klik for at åbne",
"com_ui_artifacts": "Artefakter",
"com_ui_artifacts_toggle": "Skift artefakter UI",
"com_ui_artifacts_toggle_agent": "Aktiver artefakter",

View file

@ -685,7 +685,7 @@
"com_ui_archive": "Archivieren",
"com_ui_archive_delete_error": "Archivierter Chat konnte nicht gelöscht werden.",
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
"com_ui_artifact_click": "Zum Öffnen klicken",
"com_ui_click_to_open": "Zum Öffnen klicken",
"com_ui_artifacts": "Artefakte",
"com_ui_artifacts_options": "Artefakt Optionen",
"com_ui_artifacts_toggle": "Artefakte-Funktion einschalten",

View file

@ -688,7 +688,8 @@
"com_ui_archive": "Archive",
"com_ui_archive_delete_error": "Failed to delete archived conversation",
"com_ui_archive_error": "Failed to archive conversation",
"com_ui_artifact_click": "Click to open",
"com_ui_click_to_open": "Click to open",
"com_ui_click_to_close": "Click to close",
"com_ui_artifacts": "Artifacts",
"com_ui_artifacts_options": "Artifacts Options",
"com_ui_artifacts_toggle": "Toggle Artifacts UI",

View file

@ -510,7 +510,7 @@
"com_ui_archive": "Archivar",
"com_ui_archive_delete_error": "Error al borrar conversación archivada",
"com_ui_archive_error": "Error al archivar la conversación",
"com_ui_artifact_click": "Haga clic para abrir",
"com_ui_click_to_open": "Haga clic para abrir",
"com_ui_artifacts": "Artefactos",
"com_ui_artifacts_options": "Opciones de artefactos",
"com_ui_artifacts_toggle": "Alternar Interfaz de Artefactos",

View file

@ -532,7 +532,7 @@
"com_ui_archive": "Arhiveeri",
"com_ui_archive_delete_error": "Arhiveeritud vestluse kustutamine ebaõnnestus",
"com_ui_archive_error": "Vestluse arhiveerimine ebaõnnestus",
"com_ui_artifact_click": "Klõpsa avamiseks",
"com_ui_click_to_open": "Klõpsa avamiseks",
"com_ui_artifacts": "Artefaktid",
"com_ui_artifacts_toggle": "Lülita artefaktide kasutajaliides sisse/välja",
"com_ui_artifacts_toggle_agent": "Luba artefaktid",

View file

@ -479,7 +479,7 @@
"com_ui_api_key": "کلید API",
"com_ui_archive": "آرشیو",
"com_ui_archive_error": "مکالمه بایگانی نشد",
"com_ui_artifact_click": "برای باز کردن کلیک کنید",
"com_ui_click_to_open": "برای باز کردن کلیک کنید",
"com_ui_artifacts": "مصنوعات",
"com_ui_artifacts_toggle": "تغییر رابط کاربری Artifacts",
"com_ui_artifacts_toggle_agent": "Artifacts را فعال کنید",

View file

@ -635,7 +635,7 @@
"com_ui_archive": "Archiver",
"com_ui_archive_delete_error": "Suppression de la conversation archivée échouée",
"com_ui_archive_error": "échec de l'archivage de la conversation",
"com_ui_artifact_click": "Cliquer pour ouvrir",
"com_ui_click_to_open": "Cliquer pour ouvrir",
"com_ui_artifacts": "Artefacts",
"com_ui_artifacts_options": "Options des Artefacts",
"com_ui_artifacts_toggle": "Afficher/Masquer l'interface des artefacts",

View file

@ -676,7 +676,7 @@
"com_ui_archive": "לארכיון",
"com_ui_archive_delete_error": "מחיקת השיחה מהארכיון נכשלה",
"com_ui_archive_error": "אירעה שגיאה באירכוב השיחה",
"com_ui_artifact_click": "לחץ לפתיחה",
"com_ui_click_to_open": "לחץ לפתיחה",
"com_ui_artifacts": "רכיבי תצוגה",
"com_ui_artifacts_options": "אפשרויות ארטיפקטים",
"com_ui_artifacts_toggle": "הפעל/כבה רכיבי תצוגה",

View file

@ -479,7 +479,7 @@
"com_ui_api_key": "API kulcs",
"com_ui_archive": "Archiválás",
"com_ui_archive_error": "Nem sikerült archiválni a beszélgetést",
"com_ui_artifact_click": "Kattintson a megnyitáshoz",
"com_ui_click_to_open": "Kattintson a megnyitáshoz",
"com_ui_artifacts": "Műtermékek",
"com_ui_artifacts_toggle": "Műtermék kezelőfelület váltása",
"com_ui_artifacts_toggle_agent": "Műtermékek engedélyezése",

View file

@ -541,7 +541,7 @@
"com_ui_api_key": "Chiave API",
"com_ui_archive": "Archivia",
"com_ui_archive_error": "Errore durante l'archiviazione della conversazione",
"com_ui_artifact_click": "Clicca per aprire",
"com_ui_click_to_open": "Clicca per aprire",
"com_ui_artifacts": "Artefatti",
"com_ui_artifacts_toggle": "Mostra/Nascondi Interfaccia Artefatti",
"com_ui_artifacts_toggle_agent": "Abilita artefatti",

View file

@ -576,7 +576,7 @@
"com_ui_archive": "アーカイブ",
"com_ui_archive_delete_error": "アーカイブされた会話の削除に失敗しました",
"com_ui_archive_error": "アーカイブに失敗しました。",
"com_ui_artifact_click": "クリックして開く",
"com_ui_click_to_open": "クリックして開く",
"com_ui_artifacts": "アーティファクト",
"com_ui_artifacts_options": "アーティファクト・オプション",
"com_ui_artifacts_toggle": "アーティファクト UI の切替",

View file

@ -567,7 +567,7 @@
"com_ui_archive": "아카이브",
"com_ui_archive_delete_error": "저장된 대화 삭제 실패",
"com_ui_archive_error": "대화 아카이브 실패",
"com_ui_artifact_click": "클릭하여 열기",
"com_ui_click_to_open": "클릭하여 열기",
"com_ui_artifacts": "아티팩트",
"com_ui_artifacts_options": "아티팩트 옵션",
"com_ui_artifacts_toggle": "아티팩트 UI 표시/숨기기",

View file

@ -688,7 +688,7 @@
"com_ui_archive": "Arhīvs",
"com_ui_archive_delete_error": "Neizdevās izdzēst arhivēto sarunu.",
"com_ui_archive_error": "Neizdevās arhivēt sarunu.",
"com_ui_artifact_click": "Noklikšķiniet, lai atvērtu",
"com_ui_click_to_open": "Noklikšķiniet, lai atvērtu",
"com_ui_artifacts": "Artefakti",
"com_ui_artifacts_options": "Artefaktu opcijas",
"com_ui_artifacts_toggle": "Pārslēgt artefaktu lietotāja saskarni",

View file

@ -683,7 +683,7 @@
"com_ui_archive": "Arkiver",
"com_ui_archive_delete_error": "Sletting av arkivert samtale mislyktes.",
"com_ui_archive_error": "Arkivering av samtale mislyktes.",
"com_ui_artifact_click": "Klikk for å åpne",
"com_ui_click_to_open": "Klikk for å åpne",
"com_ui_artifacts": "Artefakter",
"com_ui_artifacts_options": "Alternativer for artefakter",
"com_ui_artifacts_toggle": "Veksle artefakt-UI",

View file

@ -429,7 +429,7 @@
"com_ui_all_proper": "Wszystkie",
"com_ui_archive": "Archiwum",
"com_ui_archive_error": "Nie udało się archiwizować rozmowy",
"com_ui_artifact_click": "Kliknij, aby otworzyć",
"com_ui_click_to_open": "Kliknij, aby otworzyć",
"com_ui_artifacts": "Artefakty",
"com_ui_artifacts_toggle": "Przełącz interfejs artefaktów",
"com_ui_ascending": "Rosnąco",

View file

@ -582,7 +582,7 @@
"com_ui_api_key": "Chave API",
"com_ui_archive": "Arquivar",
"com_ui_archive_error": "Falha ao arquivar conversa",
"com_ui_artifact_click": "Clique para abrir",
"com_ui_click_to_open": "Clique para abrir",
"com_ui_artifacts": "Artefatos",
"com_ui_artifacts_toggle": "Alternar UI de Artefatos",
"com_ui_artifacts_toggle_agent": "Habilitar artefatos",

View file

@ -656,7 +656,7 @@
"com_ui_archive": "Arquivar",
"com_ui_archive_delete_error": "Falha ao eliminar conversa arquivada",
"com_ui_archive_error": "Falha ao arquivar conversa",
"com_ui_artifact_click": "Clique para abrir",
"com_ui_click_to_open": "Clique para abrir",
"com_ui_artifacts": "Artefatos",
"com_ui_artifacts_options": "Opções de Artefactos",
"com_ui_artifacts_toggle": "Alternar UI de Artefatos",

View file

@ -682,7 +682,7 @@
"com_ui_archive": "Архивировать",
"com_ui_archive_delete_error": "Не удалось удалить архивированный чат",
"com_ui_archive_error": "Не удалось заархивировать чат",
"com_ui_artifact_click": "Нажмите, чтобы открыть",
"com_ui_click_to_open": "Нажмите, чтобы открыть",
"com_ui_artifacts": "Артефакты",
"com_ui_artifacts_options": "Параметры артефактов",
"com_ui_artifacts_toggle": "Показать/скрыть артефакты",

View file

@ -477,7 +477,7 @@
"com_ui_api_key": "คีย์ API",
"com_ui_archive": "เก็บถาวร",
"com_ui_archive_error": "ไม่สามารถเก็บถาวรการสนทนา",
"com_ui_artifact_click": "คลิกเพื่อเปิด",
"com_ui_click_to_open": "คลิกเพื่อเปิด",
"com_ui_artifacts": "สิ่งประดิษฐ์",
"com_ui_artifacts_toggle": "สลับ UI สิ่งประดิษฐ์",
"com_ui_artifacts_toggle_agent": "เปิดใช้งานสิ่งประดิษฐ์",

View file

@ -430,7 +430,7 @@
"com_ui_all_proper": "Tümü",
"com_ui_archive": "Arşivle",
"com_ui_archive_error": "Konuşmayı arşivleyemedi",
"com_ui_artifact_click": "Açmak için tıklayın",
"com_ui_click_to_open": "Açmak için tıklayın",
"com_ui_artifacts": "Yapıtlar",
"com_ui_artifacts_toggle": "Yapıtlar Arayüzünü Aç/Kapat",
"com_ui_ascending": "Artan",

View file

@ -683,7 +683,7 @@
"com_ui_archive": "Архівувати",
"com_ui_archive_delete_error": "Не вдалося видалити заархівований чат",
"com_ui_archive_error": "Не вдалося заархівувати чат",
"com_ui_artifact_click": "Натисніть, щоб відкрити",
"com_ui_click_to_open": "Натисніть, щоб відкрити",
"com_ui_artifacts": "Артефакти",
"com_ui_artifacts_options": "Параметри артефактів",
"com_ui_artifacts_toggle": "Показати/приховати артефакти",

View file

@ -686,7 +686,7 @@
"com_ui_archive": "归档",
"com_ui_archive_delete_error": "删除已归档对话失败",
"com_ui_archive_error": "归档对话失败",
"com_ui_artifact_click": "点击以打开",
"com_ui_click_to_open": "点击以打开",
"com_ui_artifacts": "Artifacts",
"com_ui_artifacts_options": "Artifacts 选项",
"com_ui_artifacts_toggle": "切换 Artifacts UI",

View file

@ -548,7 +548,7 @@
"com_ui_api_key": "API 金鑰",
"com_ui_archive": "封存",
"com_ui_archive_error": "封存對話時發生錯誤",
"com_ui_artifact_click": "點擊開啟",
"com_ui_click_to_open": "點擊開啟",
"com_ui_artifacts": "成品",
"com_ui_artifacts_toggle": "切換成品介面",
"com_ui_ascending": "遞增",

View file

@ -2715,6 +2715,8 @@ html {
.animate-pulse-slow {
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* iOS-inspired smooth animations */
@keyframes fadeIn {
from {
opacity: 0;
@ -2730,10 +2732,45 @@ html {
animation: fadeIn 0.5s ease-out forwards;
}
/* Enhanced smooth scaling for interactions */
.scale-98 {
transform: scale(0.98);
}
/* Add hardware acceleration for smoother animations */
.will-change-transform {
will-change: transform;
}
.will-change-opacity {
will-change: opacity;
}
/* Prevent content flash and layout shifts in artifacts */
[data-radix-scroll-area-viewport] {
/* Prevent scrollbar from causing layout shifts */
scrollbar-gutter: stable;
}
/* Ensure smooth tab content transitions */
[role="tabpanel"] {
/* Use GPU acceleration for tab content */
transform: translateZ(0);
backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
}
/* Prevent flash of content during mounting */
[role="tabpanel"][data-state="inactive"] {
visibility: hidden;
pointer-events: none;
}
[role="tabpanel"][data-state="active"] {
visibility: visible;
pointer-events: auto;
}
/* Chat Badges Animation */
@keyframes ios-wiggle {

View file

@ -47,6 +47,69 @@ module.exports = {
'0%': { transform: 'translateX(0)' },
'100%': { transform: 'translateX(100%)' },
},
// iOS-inspired smooth animations
'artifact-slide-up': {
'0%': {
transform: 'translateY(100%) scale(0.95)',
opacity: '0',
},
'100%': {
transform: 'translateY(0) scale(1)',
opacity: '1',
},
},
'artifact-slide-down': {
'0%': {
transform: 'translateY(0) scale(1)',
opacity: '1',
},
'100%': {
transform: 'translateY(100%) scale(0.95)',
opacity: '0',
},
},
'artifact-slide-in-desktop': {
'0%': {
transform: 'translateX(20px)',
opacity: '0',
},
'100%': {
transform: 'translateX(0)',
opacity: '1',
},
},
'artifact-slide-out-desktop': {
'0%': {
transform: 'translateX(0)',
opacity: '1',
},
'100%': {
transform: 'translateX(20px)',
opacity: '0',
},
},
'backdrop-fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'backdrop-fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
'tab-slide': {
'0%': { transform: 'translateX(var(--tab-slide-from))' },
'100%': { transform: 'translateX(var(--tab-slide-to))' },
},
'thinking-appear': {
'0%': {
opacity: '0',
transform: 'scale(0.9) translateY(4px)',
},
'100%': {
opacity: '1',
transform: 'scale(1) translateY(0)',
},
},
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
@ -56,6 +119,22 @@ module.exports = {
'slide-in-left': 'slide-in-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'slide-out-left': 'slide-out-left 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
'slide-out-right': 'slide-out-right 300ms cubic-bezier(0.25, 0.1, 0.25, 1)',
// iOS-inspired smooth animations
'artifact-slide-up': 'artifact-slide-up 0.45s cubic-bezier(0.32, 0.72, 0, 1)',
'artifact-slide-down': 'artifact-slide-down 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
'artifact-slide-in-desktop':
'artifact-slide-in-desktop 0.5s cubic-bezier(0.32, 0.72, 0, 1)',
'artifact-slide-out-desktop':
'artifact-slide-out-desktop 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
'backdrop-fade-in': 'backdrop-fade-in 0.3s cubic-bezier(0.32, 0.72, 0, 1)',
'backdrop-fade-out': 'backdrop-fade-out 0.25s cubic-bezier(0.32, 0.72, 0, 1)',
'tab-slide': 'tab-slide 0.35s cubic-bezier(0.32, 0.72, 0, 1)',
'thinking-appear': 'thinking-appear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
},
transitionTimingFunction: {
ios: 'cubic-bezier(0.32, 0.72, 0, 1)',
'ios-spring': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
'ios-decelerate': 'cubic-bezier(0, 0, 0.2, 1)',
},
colors: {
gray: {