mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-22 18:26:12 +01:00
🚀 feat: Shared Links (#2772)
* ✨ feat(types): add necessary types for shared link feature * ✨ feat: add shared links functions to data service Added functions for retrieving, creating, updating, and deleting shared links and shared messages. * ✨ feat: Add useGetSharedMessages hook to fetch shared messages by shareId Adds a new hook `useGetSharedMessages` which fetches shared messages based on the provided shareId. * ✨ feat: Add share schema and data access functions to API models * ✨ feat: Add share endpoint to API The GET /api/share/${shareId} is exposed to the public, so authentication is not required. Other paths require authentication. * ♻️ refactor(utils): generalize react-query cache manipulation functions Introduces generic functions for manipulating react-query cache entries, marking a refinement in how query cache data is managed. It aims to enhance the flexibility and reusability of the cache interaction patterns within our application. - Replaced specific index names with more generic terms in queries.ts, enhancing consistency across data handling functions. - Introduced new utility functions in collection.ts for adding, updating, and deleting data entries in an InfiniteData<TCollection>. These utility functions (`addData`, `updateData`, `deleteData`, `findPage`) are designed to be re-usable across different data types and collections. - Adapted existing conversation utility functions in convos.ts to leverage these new generic utilities. * ✨ feat(shared-link): add functions to manipulate shared link cache list implemented new utility functions to handle additions, updates, and deletions in the shared link cache list. * ✨ feat: Add mutations and queries for shared links * ✨ feat(shared-link): add `Share` button to conversation list - Added a share button in each conversation in the conversation list. - Implemented functionality where clicking the share button triggers a POST request to the API. - The API checks if a share link was already created for the conversation today; if so, it returns the existing link. - If no link was created for today, the API will create a new share link and return it. - Each click on the share button results in a new API request, following the specification similar to ChatGPT's share link feature. * ♻️ refactor(hooks): generalize useNavScrolling for broader use - Modified `useNavScrolling` to accept a generic type parameter `TData`, allowing it to be used with different data structures besides `ConversationListResponse`. - Updated instances in `Nav.tsx` and `ArchivedChatsTable.tsx` to explicitly specify `ConversationListResponse` as the type argument when invoking `useNavScrolling`. * ✨ feat(settings): add shared links listing table with delete functionality in settings - Integrated a delete button for each shared link in the table, allowing users to remove links as needed. * ♻️ refactor(components): separate `EndpointIcon` from `Icon` component for standalone use * ♻️ refactor: update useGetSharedMessages to return TSharedLink - Modified the useGetSharedMessages hook to return not only a list of TMessage but also the TSharedLink itself. - This change was necessary to support displaying the title and date in the Shared Message UI, which requires data from TSharedLink. * ✨ feat(shared link): add UI for displaying shared conversations without authentication - Implemented a new UI component to display shared conversations, designed to be accessible without requiring authentication. - Reused components from the authenticated Messages module where possible. Copied and adapted components that could not be directly reused to fit the non-authenticated context. * 🔧 chore: Add translations Translate labels only. Messages remain in English as they are possibly subject to change. * ♻️ refactor: add icon and tooltip props to EditMenuButton component * moved icon and popover to arguments so that EditMenuButton can be reused. * modified so that when a ShareButton is closed, the parent DropdownMenu is also closed. * ♻️irefactor: added DropdownMenu for Export and Share * ♻️ refactor: renamed component names more intuitive * More accurate naming of the dropdown menu. * When the export button is closed, the parent dropdown menu is also closed. * 🌍 chore: updated translations * 🐞 Fix: OpenID Profile Image Download (#2757) * Add fetch requirement Fixes - error: [openidStrategy] downloadImage: Error downloading image at URL "https://graph.microsoft.com/v1.0/me/photo/$value": TypeError: response.buffer is not a function * Update openidStrategy.js --------- Co-authored-by: Danny Avila <danacordially@gmail.com> * 🚑 fix(export): Issue exporting Conversation with Assistants (#2769) * 🚑 fix(export): use content as text if content is present in the message If the endpoint is assistants, the text of the message goes into content, not message.text. * refactor(ExportModel): TypeScript, remove unused code --------- Co-authored-by: Yuichi Ohneda <ohneda@gmail.com> * 📤style: export button icon (#2752) * refactor(ShareDialog): logic and styling * refactor(ExportAndShareMenu): imports order and icon update * chore: imports * chore: imports/render logic * feat: message branching * refactor: add optional config to useGetStartupConfig * refactor: disable endpoints query * chore: fix search view styling gradient in light mode * style: ShareView gradient styling * refactor(Share): use select queries * style: shared link table buttons * localization and dark text styling * style: fix clipboard button layout shift app-wide and add localization for copy code * support assistants message content in shared links, add useCopyToClipboard, add copy buttons to Search Messages and Shared Link Messages * add localizations * comparisons --------- Co-authored-by: Yuichi Ohneda <ohneda@gmail.com> Co-authored-by: bsu3338 <bsu3338@users.noreply.github.com> Co-authored-by: Fuegovic <32828263+fuegovic@users.noreply.github.com>
This commit is contained in:
parent
38ad36c1c5
commit
f0e8cca5df
78 changed files with 4683 additions and 317 deletions
100
client/src/components/Share/Message.tsx
Normal file
100
client/src/components/Share/Message.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import type { TMessageProps } from '~/common';
|
||||
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
|
||||
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
|
||||
import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
|
||||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
import Icon from './MessageIcon';
|
||||
export default function Message(props: TMessageProps) {
|
||||
const {
|
||||
message,
|
||||
siblingIdx,
|
||||
siblingCount,
|
||||
conversation,
|
||||
setSiblingIdx,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
} = props;
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { text, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {};
|
||||
|
||||
let messageLabel = '';
|
||||
if (isCreatedByUser) {
|
||||
messageLabel = 'anonymous';
|
||||
} else {
|
||||
messageLabel = message.sender;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent">
|
||||
<div className="m-auto justify-center p-4 py-2 text-base md:gap-6 ">
|
||||
<div className="final-completion group mx-auto flex flex-1 gap-3 text-base md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5">
|
||||
<div className="relative flex flex-shrink-0 flex-col items-end">
|
||||
<div>
|
||||
<div className="pt-0.5">
|
||||
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
|
||||
<Icon message={message} conversation={conversation} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn('relative flex w-11/12 flex-col', isCreatedByUser ? '' : 'agent-turn')}
|
||||
>
|
||||
<div className="select-none font-semibold">{messageLabel}</div>
|
||||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
{/* Legacy Plugins */}
|
||||
{message?.plugin && <Plugin plugin={message?.plugin} />}
|
||||
{message?.content ? (
|
||||
<SearchContent message={message} />
|
||||
) : (
|
||||
<MessageContent
|
||||
edit={false}
|
||||
error={error}
|
||||
isLast={false}
|
||||
ask={() => ({})}
|
||||
text={text ?? ''}
|
||||
message={message}
|
||||
isSubmitting={false}
|
||||
enterEdit={() => ({})}
|
||||
unfinished={!!unfinished}
|
||||
isCreatedByUser={isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SubRow classes="text-xs">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
<MinimalHoverButtons message={message} />
|
||||
</SubRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MultiMessage
|
||||
key={messageId}
|
||||
messageId={messageId}
|
||||
messagesTree={children ?? []}
|
||||
currentEditId={currentEditId}
|
||||
setCurrentEditId={setCurrentEditId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
71
client/src/components/Share/MessageIcon.tsx
Normal file
71
client/src/components/Share/MessageIcon.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { TMessage, TPreset, Assistant } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import MessageEndpointIcon from '../Endpoints/MessageEndpointIcon';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { getIconEndpoint } from '~/utils';
|
||||
import { UserIcon } from '../svg';
|
||||
|
||||
export default function MessageIcon(
|
||||
props: Pick<TMessageProps, 'message' | 'conversation'> & {
|
||||
assistant?: false | Assistant;
|
||||
},
|
||||
) {
|
||||
const { message, conversation, assistant } = props;
|
||||
|
||||
const assistantName = assistant ? (assistant.name as string | undefined) : '';
|
||||
const assistantAvatar = assistant ? (assistant.metadata?.avatar as string | undefined) : '';
|
||||
|
||||
const messageSettings = useMemo(
|
||||
() => ({
|
||||
...(conversation ?? {}),
|
||||
...({
|
||||
...message,
|
||||
iconURL: message?.iconURL ?? '',
|
||||
} as TMessage),
|
||||
}),
|
||||
[conversation, message],
|
||||
);
|
||||
|
||||
const iconURL = messageSettings?.iconURL;
|
||||
let endpoint = messageSettings?.endpoint;
|
||||
endpoint = getIconEndpoint({ endpointsConfig: undefined, iconURL, endpoint });
|
||||
|
||||
if (!message?.isCreatedByUser && iconURL && iconURL.includes('http')) {
|
||||
return (
|
||||
<ConvoIconURL
|
||||
preset={messageSettings as typeof messageSettings & TPreset}
|
||||
context="message"
|
||||
assistantAvatar={assistantAvatar}
|
||||
assistantName={assistantName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (message?.isCreatedByUser) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className="relative flex h-9 w-9 items-center justify-center rounded-sm p-1 text-white"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageEndpointIcon
|
||||
{...messageSettings}
|
||||
endpoint={endpoint}
|
||||
iconURL={!assistant ? undefined : assistantAvatar}
|
||||
model={message?.model ?? conversation?.model}
|
||||
assistantName={assistantName}
|
||||
size={28.8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
47
client/src/components/Share/MessagesView.tsx
Normal file
47
client/src/components/Share/MessagesView.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useState } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import MultiMessage from './MultiMessage';
|
||||
|
||||
export default function MessagesView({
|
||||
messagesTree: _messagesTree,
|
||||
conversationId,
|
||||
}: {
|
||||
messagesTree?: TMessage[] | null;
|
||||
conversationId: string;
|
||||
}) {
|
||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||
return (
|
||||
<div className="flex-1 pb-[50px]">
|
||||
<div className="dark:gpt-dark-gray relative h-full">
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
||||
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
|
||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
||||
Nothing found
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<MultiMessage
|
||||
key={conversationId} // avoid internal state mixture
|
||||
messagesTree={_messagesTree}
|
||||
messageId={conversationId ?? null}
|
||||
setCurrentEditId={setCurrentEditId}
|
||||
currentEditId={currentEditId ?? null}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="dark:gpt-dark-gray group h-0 w-full flex-shrink-0 dark:border-gray-800/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
client/src/components/Share/MultiMessage.tsx
Normal file
54
client/src/components/Share/MultiMessage.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import type { TMessageProps } from '~/common';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import Message from './Message';
|
||||
import store from '~/store';
|
||||
|
||||
export default function MultiMessage({
|
||||
// messageId is used recursively here
|
||||
messageId,
|
||||
messagesTree,
|
||||
currentEditId,
|
||||
setCurrentEditId,
|
||||
}: TMessageProps) {
|
||||
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
||||
|
||||
const setSiblingIdxRev = (value: number) => {
|
||||
setSiblingIdx((messagesTree?.length ?? 0) - value - 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// reset siblingIdx when the tree changes, mostly when a new message is submitting.
|
||||
setSiblingIdx(0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messagesTree?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesTree?.length && siblingIdx >= messagesTree?.length) {
|
||||
setSiblingIdx(0);
|
||||
}
|
||||
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
|
||||
|
||||
if (!(messagesTree && messagesTree?.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = messagesTree[messagesTree.length - siblingIdx - 1];
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={message.messageId}
|
||||
message={message}
|
||||
currentEditId={currentEditId}
|
||||
setCurrentEditId={setCurrentEditId}
|
||||
siblingIdx={messagesTree.length - siblingIdx - 1}
|
||||
siblingCount={messagesTree.length}
|
||||
setSiblingIdx={setSiblingIdxRev}
|
||||
/>
|
||||
);
|
||||
}
|
||||
60
client/src/components/Share/ShareView.tsx
Normal file
60
client/src/components/Share/ShareView.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useGetSharedMessages } from 'librechat-data-provider/react-query';
|
||||
import { ShareContext } from '~/Providers';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import MessagesView from './MessagesView';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { buildTree } from '~/utils';
|
||||
import Footer from '../Chat/Footer';
|
||||
|
||||
function SharedView() {
|
||||
const localize = useLocalize();
|
||||
const { shareId } = useParams();
|
||||
const { data, isLoading } = useGetSharedMessages(shareId ?? '');
|
||||
const dataTree = data && buildTree({ messages: data.messages });
|
||||
const messagesTree = dataTree?.length === 0 ? null : dataTree ?? null;
|
||||
|
||||
return (
|
||||
<ShareContext.Provider value={{ isSharedConvo: true }}>
|
||||
<div
|
||||
className="relative flex w-full grow overflow-hidden bg-white dark:bg-gray-800"
|
||||
style={{ paddingBottom: '50px' }}
|
||||
>
|
||||
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-0 dark:bg-gray-800">
|
||||
<div className="flex h-full flex-col" role="presentation" tabIndex={0}>
|
||||
{isLoading ? (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Spinner className="" />
|
||||
</div>
|
||||
) : data && messagesTree && messagesTree.length !== 0 ? (
|
||||
<>
|
||||
<div className="final-completion group mx-auto flex min-w-[40rem] flex-col gap-3 pb-6 pt-4 md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5">
|
||||
<h1 className="text-4xl font-bold dark:text-white">{data.title}</h1>
|
||||
<div className="border-b pb-6 text-base text-gray-300">
|
||||
{new Date(data.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MessagesView messagesTree={messagesTree} conversationId={data.conversationId} />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
{localize('com_ui_shared_link_not_found')}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
|
||||
<Footer className="fixed bottom-0 left-0 right-0 z-50 flex items-center justify-center gap-2 bg-gradient-to-t from-gray-50 to-transparent px-2 pb-2 pt-8 text-xs text-gray-600 dark:from-gray-800 dark:text-gray-300 md:px-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShareContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SharedView);
|
||||
Loading…
Add table
Add a link
Reference in a new issue