mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 16:23:44 +00:00
feat: import shared convos
This commit is contained in:
parent
50a48efa43
commit
503b7f4d8d
6 changed files with 126 additions and 8 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
<ShareMessagesProvider messages={data.messages}>
|
||||
<MessagesView messagesTree={messagesTree} conversationId="shared-conversation" />
|
||||
|
|
@ -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({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<OGDialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<OGDialog open={settingsOpen} onOpenChange={handleSettingsOpenChange}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
size={isMobile ? 'icon' : 'default'}
|
||||
|
|
@ -237,6 +314,34 @@ function ShareHeader({
|
|||
<div className="relative focus-within:z-[100]">
|
||||
<LangSelector langcode={langcode} onChange={onLangChange} portal={false} />
|
||||
</div>
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<div className="bg-border-medium/60 h-px w-full" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-text-primary">
|
||||
{localize('com_ui_import_conversation')}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isImporting}
|
||||
onClick={handleImport}
|
||||
aria-label={localize('com_ui_import')}
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<Spinner className="mr-1 w-4" />
|
||||
<span>{localize('com_ui_importing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{localize('com_ui_import')}</span>
|
||||
<ExternalLink className="ml-1 h-4 w-4" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -520,6 +520,10 @@ export type TImportResponse = {
|
|||
* The message associated with the response.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* The IDs of the imported conversations.
|
||||
*/
|
||||
conversationIds?: string[];
|
||||
};
|
||||
|
||||
/** Prompts */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue