LibreChat/client/src/components/Share/ShareArtifacts.tsx
Marco Beretta c2505d2bc9
🤝 feat: View Artifacts in Shared Conversations (#10477)
* feat: Integrate logger for MessageIcon component

* feat: Enhance artifact sharing functionality with updated path checks and read-only state management

* feat: Refactor Thinking and Reasoning components for improved structure and styling

* feat: Enhance artifact sharing with context value management and responsive layout

* feat: Enhance ShareView with theme and language management features

* feat: Improve ThinkingButton accessibility and styling for better user interaction

* feat: Introduce isArtifactRoute utility for route validation in Artifact components

* feat: Add latest message text extraction in SharedView for improved message display

* feat: Update locale handling in SharedView for dynamic date formatting

* feat: Refactor ArtifactsContext and SharedView for improved context handling and styling adjustments

* feat: Enhance artifact panel size management with local storage integration

* chore: imports

* refactor: move ShareArtifactsContainer out of ShareView

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-11-13 16:59:46 -05:00

176 lines
5.1 KiB
TypeScript

import { useState, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import {
useMediaQuery,
ResizablePanel,
ResizableHandleAlt,
ResizablePanelGroup,
} from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import type { ArtifactsContextValue } from '~/Providers';
import { ArtifactsProvider, EditorProvider } from '~/Providers';
import Artifacts from '~/components/Artifacts/Artifacts';
import { getLatestText } from '~/utils';
import store from '~/store';
const DEFAULT_ARTIFACT_PANEL_SIZE = 40;
const SHARE_ARTIFACT_PANEL_STORAGE_KEY = 'share:artifacts-panel-size';
const SHARE_ARTIFACT_PANEL_DEFAULT_KEY = 'share:artifacts-panel-size-default';
/**
* Gets the initial artifact panel size from localStorage or returns default
*/
const getInitialArtifactPanelSize = () => {
if (typeof window === 'undefined') {
return DEFAULT_ARTIFACT_PANEL_SIZE;
}
const defaultSizeString = String(DEFAULT_ARTIFACT_PANEL_SIZE);
const storedDefault = window.localStorage.getItem(SHARE_ARTIFACT_PANEL_DEFAULT_KEY);
if (storedDefault !== defaultSizeString) {
window.localStorage.setItem(SHARE_ARTIFACT_PANEL_DEFAULT_KEY, defaultSizeString);
window.localStorage.removeItem(SHARE_ARTIFACT_PANEL_STORAGE_KEY);
return DEFAULT_ARTIFACT_PANEL_SIZE;
}
const stored = window.localStorage.getItem(SHARE_ARTIFACT_PANEL_STORAGE_KEY);
const parsed = Number(stored);
return Number.isFinite(parsed) ? parsed : DEFAULT_ARTIFACT_PANEL_SIZE;
};
interface ShareArtifactsContainerProps {
messages: TMessage[];
conversationId: string;
mainContent: React.ReactNode;
}
/**
* Container component that manages artifact visibility and layout for shared conversations
*/
export function ShareArtifactsContainer({
messages,
conversationId,
mainContent,
}: ShareArtifactsContainerProps) {
const artifacts = useRecoilValue(store.artifactsState);
const artifactsVisibility = useRecoilValue(store.artifactsVisibility);
const isSmallScreen = useMediaQuery('(max-width: 1023px)');
const [artifactPanelSize, setArtifactPanelSize] = useState(getInitialArtifactPanelSize);
const artifactsContextValue = useMemo<ArtifactsContextValue | null>(() => {
const latestMessage =
Array.isArray(messages) && messages.length > 0 ? messages[messages.length - 1] : null;
if (!latestMessage) {
return null;
}
const latestMessageText = getLatestText(latestMessage);
return {
isSubmitting: false,
latestMessageId: latestMessage.messageId ?? null,
latestMessageText,
conversationId: conversationId ?? null,
};
}, [messages, conversationId]);
const shouldRenderArtifacts =
artifactsVisibility === true &&
artifactsContextValue != null &&
Object.keys(artifacts ?? {}).length > 0;
const normalizedArtifactSize = Math.min(60, Math.max(20, artifactPanelSize));
/**
* Handles artifact panel resize and persists size to localStorage
*/
const handleLayoutChange = (sizes: number[]) => {
if (sizes.length < 2) {
return;
}
const newSize = sizes[1];
if (!Number.isFinite(newSize)) {
return;
}
setArtifactPanelSize(newSize);
if (typeof window !== 'undefined') {
window.localStorage.setItem(SHARE_ARTIFACT_PANEL_STORAGE_KEY, newSize.toString());
}
};
if (!shouldRenderArtifacts || !artifactsContextValue) {
return <>{mainContent}</>;
}
if (isSmallScreen) {
return (
<>
{mainContent}
<ShareArtifactsOverlay contextValue={artifactsContextValue} />
</>
);
}
return (
<ResizablePanelGroup
direction="horizontal"
className="flex h-full w-full"
onLayout={handleLayoutChange}
>
<ResizablePanel
defaultSize={100 - normalizedArtifactSize}
minSize={35}
order={1}
id="share-content"
>
{mainContent}
</ResizablePanel>
<ResizableHandleAlt withHandle className="bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={normalizedArtifactSize}
minSize={20}
maxSize={60}
order={2}
id="share-artifacts"
>
<ShareArtifactsPanel contextValue={artifactsContextValue} />
</ResizablePanel>
</ResizablePanelGroup>
);
}
interface ShareArtifactsPanelProps {
contextValue: ArtifactsContextValue;
}
/**
* Panel that renders the artifacts UI within a resizable container
*/
function ShareArtifactsPanel({ contextValue }: ShareArtifactsPanelProps) {
return (
<ArtifactsProvider value={contextValue}>
<EditorProvider>
<div className="flex h-full w-full border-l border-border-light bg-surface-primary shadow-2xl">
<Artifacts />
</div>
</EditorProvider>
</ArtifactsProvider>
);
}
/**
* Mobile overlay that displays artifacts in a fixed position
*/
function ShareArtifactsOverlay({ contextValue }: ShareArtifactsPanelProps) {
return (
<div
className="fixed inset-y-0 right-0 z-40 flex w-full max-w-full sm:max-w-[420px]"
role="complementary"
aria-label="Artifacts panel"
>
<ShareArtifactsPanel contextValue={contextValue} />
</div>
);
}