diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index bb9c4ebea9..cf1857ef4b 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -242,8 +242,11 @@ router.post( async (req, res) => { try { /* TODO: optimize to return imported conversations and add manually */ - await importConversations({ filepath: req.file.path, requestUserId: req.user.id }); - res.status(201).json({ message: 'Conversation(s) imported successfully' }); + const conversationIds = await importConversations({ + filepath: req.file.path, + requestUserId: req.user.id, + }); + res.status(201).json({ message: 'Conversation(s) imported successfully', conversationIds }); } catch (error) { logger.error('Error processing file', error); res.status(500).send('Error processing file'); diff --git a/api/server/utils/import/importConversations.js b/api/server/utils/import/importConversations.js index d9e4d4332d..1f3a8efcde 100644 --- a/api/server/utils/import/importConversations.js +++ b/api/server/utils/import/importConversations.js @@ -22,8 +22,9 @@ const importConversations = async (job) => { const fileData = await fs.readFile(filepath, 'utf8'); const jsonData = JSON.parse(fileData); const importer = getImporter(jsonData); - await importer(jsonData, requestUserId); + const conversationIds = await importer(jsonData, requestUserId); logger.debug(`user: ${requestUserId} | Finished importing conversations`); + return conversationIds; } catch (error) { logger.error(`user: ${requestUserId} | Failed to import conversation: `, error); throw error; // throw error all the way up so request does not return success diff --git a/api/server/utils/import/importers.js b/api/server/utils/import/importers.js index 81a0f048df..87a48a836d 100644 --- a/api/server/utils/import/importers.js +++ b/api/server/utils/import/importers.js @@ -72,6 +72,7 @@ async function importChatBotUiConvo( } await importBatchBuilder.saveBatch(); logger.info(`user: ${requestUserId} | ChatbotUI conversation imported`); + return importBatchBuilder.conversations.map((c) => c.conversationId); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from ChatbotUI file`, error); } @@ -177,6 +178,7 @@ async function importClaudeConvo( await importBatchBuilder.saveBatch(); logger.info(`user: ${requestUserId} | Claude conversation imported`); + return importBatchBuilder.conversations.map((c) => c.conversationId); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from Claude file`, error); } @@ -272,6 +274,7 @@ async function importLibreChatConvo( importBatchBuilder.finishConversation(jsonData.title, firstMessageDate ?? new Date(), options); await importBatchBuilder.saveBatch(); logger.debug(`user: ${requestUserId} | Conversation "${jsonData.title}" imported`); + return importBatchBuilder.conversations.map((c) => c.conversationId); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from LibreChat file`, error); } @@ -297,6 +300,7 @@ async function importChatGptConvo( processConversation(conv, importBatchBuilder, requestUserId); } await importBatchBuilder.saveBatch(); + return importBatchBuilder.conversations.map((c) => c.conversationId); } catch (error) { logger.error(`user: ${requestUserId} | Error creating conversation from imported file`, error); } diff --git a/client/src/components/Share/ShareView.tsx b/client/src/components/Share/ShareView.tsx index 99ab7f35eb..c811bd3595 100644 --- a/client/src/components/Share/ShareView.tsx +++ b/client/src/components/Share/ShareView.tsx @@ -2,9 +2,10 @@ 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 { useQuery } from '@tanstack/react-query'; +import { CalendarDays, Settings, ExternalLink } from 'lucide-react'; import { useGetSharedMessages } from 'librechat-data-provider/react-query'; +import { buildTree, request, setTokenHeader } from 'librechat-data-provider'; import { Spinner, Button, @@ -13,26 +14,48 @@ import { OGDialogTitle, useMediaQuery, OGDialogHeader, + useToastContext, OGDialogContent, OGDialogTrigger, } from '@librechat/client'; import { ThemeSelector, LangSelector } from '~/components/Nav/SettingsTabs/General/General'; +import { useGetStartupConfig, useUploadConversationsMutation } from '~/data-provider'; +import { ShareMessagesProvider } from './ShareMessagesProvider'; import { ShareArtifactsContainer } from './ShareArtifacts'; import { useLocalize, useDocumentTitle } from '~/hooks'; -import { useGetStartupConfig } from '~/data-provider'; +import { NotificationSeverity } from '~/common'; 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 useOptionalAuth() { + const [enabled, setEnabled] = useState(false); + const { data: isAuthenticated = false } = useQuery({ + queryKey: ['optionalAuth'], + queryFn: async () => { + const res = await request.refreshToken(); + if (res?.token) { + setTokenHeader(res.token); + return true; + } + return false; + }, + enabled, + retry: false, + refetchOnWindowFocus: false, + }); + return { isAuthenticated, checkAuth: () => setEnabled(true) }; +} + function SharedView() { const localize = useLocalize(); const { data: config } = useGetStartupConfig(); const { theme, setTheme } = useContext(ThemeContext); const { shareId } = useParams(); const { data, isLoading } = useGetSharedMessages(shareId ?? ''); + const { isAuthenticated, checkAuth } = useOptionalAuth(); const dataTree = data && buildTree({ messages: data.messages }); const messagesTree = dataTree?.length === 0 ? null : (dataTree ?? null); @@ -108,6 +131,9 @@ function SharedView() { onThemeChange={handleThemeChange} onLangChange={handleLangChange} settingsLabel={localize('com_nav_settings')} + isAuthenticated={isAuthenticated} + onSettingsOpen={checkAuth} + sharedData={data} /> @@ -165,6 +191,9 @@ interface ShareHeaderProps { theme: string; langcode: string; settingsLabel: string; + isAuthenticated: boolean; + onSettingsOpen: () => void; + sharedData: { conversationId: string; title: string; messages: unknown[] } | undefined; onThemeChange: (value: string) => void; onLangChange: (value: string) => void; } @@ -175,12 +204,60 @@ function ShareHeader({ theme, langcode, settingsLabel, + isAuthenticated, + onSettingsOpen, + sharedData, onThemeChange, onLangChange, }: ShareHeaderProps) { + const localize = useLocalize(); + const { showToast } = useToastContext(); const [settingsOpen, setSettingsOpen] = useState(false); + const [isImporting, setIsImporting] = useState(false); const isMobile = useMediaQuery('(max-width: 767px)'); + const handleSettingsOpenChange = useCallback( + (open: boolean) => { + setSettingsOpen(open); + if (open) { + onSettingsOpen(); + } + }, + [onSettingsOpen], + ); + + const uploadConversation = useUploadConversationsMutation({ + onSuccess: (data) => { + setIsImporting(false); + const id = data?.conversationIds?.[0]; + window.location.href = id ? `/c/${id}` : '/c/new'; + }, + onError: () => { + showToast({ + message: localize('com_ui_import_conversation_error'), + status: NotificationSeverity.ERROR, + }); + setIsImporting(false); + }, + onMutate: () => setIsImporting(true), + }); + + const handleImport = useCallback(() => { + if (!sharedData) { + return; + } + const json = JSON.stringify({ + conversationId: 'import', // A new, valid UUID is automatically generated by the server on upload + title: sharedData.title, + messages: sharedData.messages, + }); + const blob = new Blob([json], { type: 'application/json' }); + const file = new File([blob], 'shared-conversation.json', { type: 'application/json' }); + const formData = new FormData(); + formData.append('file', file, file.name); + uploadConversation.mutate(formData); + }, [sharedData, uploadConversation]); + const handleDialogOutside = useCallback((event: Event) => { const target = event.target as HTMLElement | null; if (target?.closest('[data-dialog-ignore="true"]')) { @@ -202,7 +279,7 @@ function ShareHeader({ )} - + + + + )} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index e0dad68431..5d28791dd2 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1059,6 +1059,7 @@ "com_ui_image_edited": "Image edited", "com_ui_image_gen": "Image Gen", "com_ui_import": "Import", + "com_ui_import_conversation": "Import Conversation", "com_ui_import_conversation_error": "There was an error importing your conversations", "com_ui_import_conversation_file_type_error": "Unsupported import type", "com_ui_import_conversation_info": "Import conversations from a JSON file", diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index a7782a3bc6..8d6972c6d0 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -520,6 +520,10 @@ export type TImportResponse = { * The message associated with the response. */ message: string; + /** + * The IDs of the imported conversations. + */ + conversationIds?: string[]; }; /** Prompts */