mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-27 05:38:51 +01:00
* 💻 feat: deeper MCP UI integration in the chat UI using plugins --------- Co-authored-by: Samuel Path <samuel.path@shopify.com> Co-authored-by: Pierre-Luc Godin <pierreluc.godin@shopify.com> * 💻 refactor: Migrate MCP UI resources from index-based to ID-based referencing - Replace index-based resource markers with stable resource IDs - Update plugin to parse \ui{resourceId} format instead of \ui0 - Refactor components to use useMessagesOperations instead of useSubmitMessage - Add ShareMessagesProvider for UI resources in share view - Add useConversationUIResources hook for cross-turn resource lookups - Update parsers to generate resource IDs from content hashes - Update all tests to use resource IDs instead of indices - Add sandbox permissions for iframe popups - Remove deprecated MCP tool context instructions --------- Co-authored-by: Pierre-Luc Godin <pierreluc.godin@shopify.com>
249 lines
8.1 KiB
TypeScript
249 lines
8.1 KiB
TypeScript
import { memo, useState, useCallback, useContext } from 'react';
|
|
import Cookies from 'js-cookie';
|
|
import { useRecoilState } from 'recoil';
|
|
import { useParams } from 'react-router-dom';
|
|
import { buildTree } from 'librechat-data-provider';
|
|
import { CalendarDays, Settings } from 'lucide-react';
|
|
import { useGetSharedMessages } from 'librechat-data-provider/react-query';
|
|
import {
|
|
Spinner,
|
|
Button,
|
|
OGDialog,
|
|
ThemeContext,
|
|
OGDialogTitle,
|
|
useMediaQuery,
|
|
OGDialogHeader,
|
|
OGDialogContent,
|
|
OGDialogTrigger,
|
|
} from '@librechat/client';
|
|
import { ThemeSelector, LangSelector } from '~/components/Nav/SettingsTabs/General/General';
|
|
import { ShareArtifactsContainer } from './ShareArtifacts';
|
|
import { useLocalize, useDocumentTitle } from '~/hooks';
|
|
import { useGetStartupConfig } from '~/data-provider';
|
|
import { ShareContext } from '~/Providers';
|
|
import { ShareMessagesProvider } from './ShareMessagesProvider';
|
|
import MessagesView from './MessagesView';
|
|
import Footer from '../Chat/Footer';
|
|
import { cn } from '~/utils';
|
|
import store from '~/store';
|
|
|
|
function SharedView() {
|
|
const localize = useLocalize();
|
|
const { data: config } = useGetStartupConfig();
|
|
const { theme, setTheme } = useContext(ThemeContext);
|
|
const { shareId } = useParams();
|
|
const { data, isLoading } = useGetSharedMessages(shareId ?? '');
|
|
const dataTree = data && buildTree({ messages: data.messages });
|
|
const messagesTree = dataTree?.length === 0 ? null : (dataTree ?? null);
|
|
|
|
const [langcode, setLangcode] = useRecoilState(store.lang);
|
|
|
|
// configure document title
|
|
let docTitle = '';
|
|
if (config?.appTitle != null && data?.title != null) {
|
|
docTitle = `${data.title} | ${config.appTitle}`;
|
|
} else {
|
|
docTitle = data?.title ?? config?.appTitle ?? document.title;
|
|
}
|
|
|
|
useDocumentTitle(docTitle);
|
|
|
|
const locale =
|
|
langcode ||
|
|
(typeof navigator !== 'undefined'
|
|
? navigator.language || navigator.languages?.[0] || 'en-US'
|
|
: 'en-US');
|
|
|
|
const formattedDate =
|
|
data?.createdAt != null
|
|
? new Date(data.createdAt).toLocaleDateString(locale, {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})
|
|
: null;
|
|
|
|
const handleThemeChange = useCallback(
|
|
(value: string) => {
|
|
setTheme(value);
|
|
},
|
|
[setTheme],
|
|
);
|
|
|
|
const handleLangChange = useCallback(
|
|
(value: string) => {
|
|
let userLang = value;
|
|
if (value === 'auto') {
|
|
userLang =
|
|
(typeof navigator !== 'undefined'
|
|
? navigator.language || navigator.languages?.[0]
|
|
: null) ?? 'en-US';
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
document.documentElement.lang = userLang;
|
|
});
|
|
|
|
setLangcode(userLang);
|
|
Cookies.set('lang', userLang, { expires: 365 });
|
|
},
|
|
[setLangcode],
|
|
);
|
|
|
|
let content: JSX.Element;
|
|
if (isLoading) {
|
|
content = (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<Spinner className="" />
|
|
</div>
|
|
);
|
|
} else if (data && messagesTree && messagesTree.length !== 0) {
|
|
content = (
|
|
<>
|
|
<ShareHeader
|
|
title={data.title}
|
|
formattedDate={formattedDate}
|
|
theme={theme}
|
|
langcode={langcode}
|
|
onThemeChange={handleThemeChange}
|
|
onLangChange={handleLangChange}
|
|
settingsLabel={localize('com_nav_settings')}
|
|
/>
|
|
<ShareMessagesProvider messages={data.messages}>
|
|
<MessagesView messagesTree={messagesTree} conversationId="shared-conversation" />
|
|
</ShareMessagesProvider>
|
|
</>
|
|
);
|
|
} else {
|
|
content = (
|
|
<div className="flex h-screen items-center justify-center">
|
|
{localize('com_ui_shared_link_not_found')}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const footer = (
|
|
<div className="w-full border-t-0 pl-0 pt-2 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
|
<Footer className="relative mx-auto mt-4 flex max-w-[55rem] flex-wrap items-center justify-center gap-2 px-3 pb-4 pt-2 text-center text-xs text-text-secondary" />
|
|
</div>
|
|
);
|
|
|
|
const mainContent = (
|
|
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden pt-0 dark:bg-surface-secondary">
|
|
<div className="flex h-full flex-col text-text-primary" role="presentation">
|
|
{content}
|
|
{footer}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const artifactsContainer =
|
|
data && data.messages ? (
|
|
<ShareArtifactsContainer
|
|
messages={data.messages}
|
|
conversationId={data.conversationId}
|
|
mainContent={mainContent}
|
|
/>
|
|
) : (
|
|
mainContent
|
|
);
|
|
|
|
return (
|
|
<ShareContext.Provider value={{ isSharedConvo: true }}>
|
|
<div className="relative flex min-h-screen w-full dark:bg-surface-secondary">
|
|
<main className="relative flex w-full grow overflow-hidden dark:bg-surface-secondary">
|
|
{artifactsContainer}
|
|
</main>
|
|
</div>
|
|
</ShareContext.Provider>
|
|
);
|
|
}
|
|
|
|
interface ShareHeaderProps {
|
|
title?: string;
|
|
formattedDate: string | null;
|
|
theme: string;
|
|
langcode: string;
|
|
settingsLabel: string;
|
|
onThemeChange: (value: string) => void;
|
|
onLangChange: (value: string) => void;
|
|
}
|
|
|
|
function ShareHeader({
|
|
title,
|
|
formattedDate,
|
|
theme,
|
|
langcode,
|
|
settingsLabel,
|
|
onThemeChange,
|
|
onLangChange,
|
|
}: ShareHeaderProps) {
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const isMobile = useMediaQuery('(max-width: 767px)');
|
|
|
|
const handleDialogOutside = useCallback((event: Event) => {
|
|
const target = event.target as HTMLElement | null;
|
|
if (target?.closest('[data-dialog-ignore="true"]')) {
|
|
event.preventDefault();
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<section className="mx-auto w-full px-3 pb-4 pt-6 md:px-5">
|
|
<div className="bg-surface-primary/80 relative mx-auto flex w-full max-w-[60rem] flex-col gap-4 rounded-3xl border border-border-light px-6 py-5 shadow-xl backdrop-blur">
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
|
<div className="space-y-2">
|
|
<h1 className="text-4xl font-semibold text-text-primary">{title}</h1>
|
|
{formattedDate && (
|
|
<div className="flex items-center gap-2 text-sm text-text-secondary">
|
|
<CalendarDays className="size-4" aria-hidden="true" />
|
|
<span>{formattedDate}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<OGDialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
|
<OGDialogTrigger asChild>
|
|
<Button
|
|
size={isMobile ? 'icon' : 'default'}
|
|
type="button"
|
|
variant="outline"
|
|
aria-label={settingsLabel}
|
|
className={cn(
|
|
'rounded-full border-border-medium text-sm text-text-primary transition-colors',
|
|
isMobile
|
|
? 'absolute bottom-4 right-4 justify-center p-0 shadow-lg'
|
|
: 'gap-2 self-start px-4 py-2',
|
|
)}
|
|
>
|
|
<Settings className="size-4" aria-hidden="true" />
|
|
<span className="hidden md:inline">{settingsLabel}</span>
|
|
</Button>
|
|
</OGDialogTrigger>
|
|
<OGDialogContent
|
|
className="w-11/12 max-w-lg"
|
|
showCloseButton={true}
|
|
onPointerDownOutside={handleDialogOutside}
|
|
onInteractOutside={handleDialogOutside}
|
|
>
|
|
<OGDialogHeader className="text-left">
|
|
<OGDialogTitle>{settingsLabel}</OGDialogTitle>
|
|
</OGDialogHeader>
|
|
<div className="flex flex-col gap-4 pt-2 text-sm">
|
|
<div className="relative focus-within:z-[100]">
|
|
<ThemeSelector theme={theme} onChange={onThemeChange} portal={false} />
|
|
</div>
|
|
<div className="bg-border-medium/60 h-px w-full" />
|
|
<div className="relative focus-within:z-[100]">
|
|
<LangSelector langcode={langcode} onChange={onLangChange} portal={false} />
|
|
</div>
|
|
</div>
|
|
</OGDialogContent>
|
|
</OGDialog>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default memo(SharedView);
|