| | 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 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); |
| |
|
| | |
| | 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')} |
| | /> |
| | <MessagesView messagesTree={messagesTree} conversationId={data.conversationId} /> |
| | </> |
| | ); |
| | } 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); |
| |
|