mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-21 01:44:09 +01:00
* ✨ 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>
148 lines
5.1 KiB
TypeScript
148 lines
5.1 KiB
TypeScript
import { useMemo, useState, useEffect, useRef } from 'react';
|
|
import { Constants } from 'librechat-data-provider';
|
|
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
|
|
import { useArtifactsContext } from '~/Providers';
|
|
import { logger } from '~/utils';
|
|
import store from '~/store';
|
|
|
|
export default function useArtifacts() {
|
|
const [activeTab, setActiveTab] = useState('preview');
|
|
const { isSubmitting, latestMessageId, latestMessageText, conversationId } =
|
|
useArtifactsContext();
|
|
|
|
const artifacts = useRecoilValue(store.artifactsState);
|
|
const resetArtifacts = useResetRecoilState(store.artifactsState);
|
|
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
|
|
const [currentArtifactId, setCurrentArtifactId] = useRecoilState(store.currentArtifactId);
|
|
|
|
const orderedArtifactIds = useMemo(() => {
|
|
return Object.keys(artifacts ?? {}).sort(
|
|
(a, b) => (artifacts?.[a]?.lastUpdateTime ?? 0) - (artifacts?.[b]?.lastUpdateTime ?? 0),
|
|
);
|
|
}, [artifacts]);
|
|
|
|
const prevIsSubmittingRef = useRef<boolean>(false);
|
|
const lastContentRef = useRef<string | null>(null);
|
|
const hasEnclosedArtifactRef = useRef<boolean>(false);
|
|
const hasAutoSwitchedToCodeRef = useRef<boolean>(false);
|
|
const lastRunMessageIdRef = useRef<string | null>(null);
|
|
const prevConversationIdRef = useRef<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const resetState = () => {
|
|
resetArtifacts();
|
|
resetCurrentArtifactId();
|
|
prevConversationIdRef.current = conversationId;
|
|
lastRunMessageIdRef.current = null;
|
|
lastContentRef.current = null;
|
|
hasEnclosedArtifactRef.current = false;
|
|
hasAutoSwitchedToCodeRef.current = false;
|
|
};
|
|
if (conversationId !== prevConversationIdRef.current && prevConversationIdRef.current != null) {
|
|
resetState();
|
|
} else if (conversationId === Constants.NEW_CONVO) {
|
|
resetState();
|
|
}
|
|
prevConversationIdRef.current = conversationId;
|
|
/** Resets artifacts when unmounting */
|
|
return () => {
|
|
logger.log('artifacts_visibility', 'Unmounting artifacts');
|
|
resetState();
|
|
};
|
|
}, [conversationId, resetArtifacts, resetCurrentArtifactId]);
|
|
|
|
useEffect(() => {
|
|
if (orderedArtifactIds.length > 0) {
|
|
const latestArtifactId = orderedArtifactIds[orderedArtifactIds.length - 1];
|
|
setCurrentArtifactId(latestArtifactId);
|
|
}
|
|
}, [setCurrentArtifactId, orderedArtifactIds]);
|
|
|
|
/**
|
|
* Manage artifact selection and code tab switching for non-enclosed artifacts
|
|
* Runs when artifact content changes
|
|
*/
|
|
useEffect(() => {
|
|
// Check if we just finished submitting (transition from true to false)
|
|
const justFinishedSubmitting = prevIsSubmittingRef.current && !isSubmitting;
|
|
prevIsSubmittingRef.current = isSubmitting;
|
|
|
|
// Only process during submission OR when just finished
|
|
if (!isSubmitting && !justFinishedSubmitting) {
|
|
return;
|
|
}
|
|
if (orderedArtifactIds.length === 0) {
|
|
return;
|
|
}
|
|
if (latestMessageId == null) {
|
|
return;
|
|
}
|
|
const latestArtifactId = orderedArtifactIds[orderedArtifactIds.length - 1];
|
|
const latestArtifact = artifacts?.[latestArtifactId];
|
|
if (latestArtifact?.content === lastContentRef.current && !justFinishedSubmitting) {
|
|
return;
|
|
}
|
|
|
|
setCurrentArtifactId(latestArtifactId);
|
|
lastContentRef.current = latestArtifact?.content ?? null;
|
|
|
|
// Only switch to code tab if we haven't detected an enclosed artifact yet
|
|
if (!hasEnclosedArtifactRef.current && !hasAutoSwitchedToCodeRef.current) {
|
|
const artifactStartContent = latestArtifact?.content?.slice(0, 50) ?? '';
|
|
if (artifactStartContent.length > 0 && latestMessageText.includes(artifactStartContent)) {
|
|
setActiveTab('code');
|
|
hasAutoSwitchedToCodeRef.current = true;
|
|
}
|
|
}
|
|
}, [
|
|
artifacts,
|
|
isSubmitting,
|
|
latestMessageId,
|
|
latestMessageText,
|
|
orderedArtifactIds,
|
|
setCurrentArtifactId,
|
|
]);
|
|
|
|
/**
|
|
* Watch for enclosed artifact pattern during message generation
|
|
* Optimized: Exits early if already detected, only checks during streaming
|
|
*/
|
|
useEffect(() => {
|
|
if (!isSubmitting || hasEnclosedArtifactRef.current) {
|
|
return;
|
|
}
|
|
|
|
const hasEnclosedArtifact =
|
|
/:::artifact(?:\{[^}]*\})?(?:\s|\n)*(?:```[\s\S]*?```(?:\s|\n)*)?:::/m.test(
|
|
latestMessageText.trim(),
|
|
);
|
|
|
|
if (hasEnclosedArtifact) {
|
|
logger.log('artifacts', 'Enclosed artifact detected during generation, switching to preview');
|
|
setActiveTab('preview');
|
|
hasEnclosedArtifactRef.current = true;
|
|
hasAutoSwitchedToCodeRef.current = false;
|
|
}
|
|
}, [isSubmitting, latestMessageText]);
|
|
|
|
useEffect(() => {
|
|
if (latestMessageId !== lastRunMessageIdRef.current) {
|
|
lastRunMessageIdRef.current = latestMessageId;
|
|
hasEnclosedArtifactRef.current = false;
|
|
hasAutoSwitchedToCodeRef.current = false;
|
|
}
|
|
}, [latestMessageId]);
|
|
|
|
const currentArtifact = currentArtifactId != null ? artifacts?.[currentArtifactId] : null;
|
|
|
|
const currentIndex = orderedArtifactIds.indexOf(currentArtifactId ?? '');
|
|
|
|
return {
|
|
activeTab,
|
|
setActiveTab,
|
|
currentIndex,
|
|
currentArtifact,
|
|
orderedArtifactIds,
|
|
setCurrentArtifactId,
|
|
};
|
|
}
|