diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index dc23620a12..38e7636e25 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -677,7 +677,9 @@ export const calculateSHA256 = async (file) => { export const getImportOrigin = (_chats) => { // Check what external service chat imports are from - if ('mapping' in _chats[0]) { + // ChatGPT exports may include folder/project metadata entries without 'mapping', + // so we check if ANY item has a 'mapping' key instead of only the first one. + if (_chats.some((chat) => 'mapping' in chat)) { return 'openai'; } return 'webui'; @@ -706,6 +708,21 @@ export const getUserPosition = async (raw = false) => { } }; +const extractOpenAIMessageContent = (message): string => { + // Extract text content from a ChatGPT message, handling various content formats + // (string parts, object parts like DALL-E images, text field fallback) + try { + const parts = message?.['content']?.['parts']; + if (Array.isArray(parts)) { + const textParts = parts.filter((p) => typeof p === 'string'); + if (textParts.length > 0) return textParts.join('\n'); + } + return message?.['content']?.['text'] || ''; + } catch { + return ''; + } +}; + const convertOpenAIMessages = (convo) => { // Parse OpenAI chat messages and create chat dictionary for creating new chats const mapping = convo['mapping']; @@ -726,15 +743,18 @@ const convertOpenAIMessages = (convo) => { // Skip chat messages with no content continue; } else { + const role = message['message']?.['author']?.['role']; + // Skip system and tool messages — they don't map to user/assistant + if (role === 'system' || role === 'tool') { + continue; + } + const new_chat = { id: message_id, parentId: lastId, childrenIds: message['children'] || [], - role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user', - content: - message['message']?.['content']?.['parts']?.[0] || - message['message']?.['content']?.['text'] || - '', + role: role !== 'user' ? 'assistant' : 'user', + content: extractOpenAIMessageContent(message['message']), model: 'gpt-3.5-turbo', done: true, context: null @@ -747,6 +767,12 @@ const convertOpenAIMessages = (convo) => { } } + // Fix up the last message's childrenIds to be empty (it's the leaf node in our + // linear chain regardless of what the original tree structure had) + if (messages.length > 0) { + messages[messages.length - 1].childrenIds = []; + } + const history: Record = {}; messages.forEach((obj) => (history[obj.id] = obj)); @@ -773,18 +799,6 @@ const validateChat = (chat) => { return false; } - // Last message's children should be an empty array - const lastMessage = messages[messages.length - 1]; - if (lastMessage.childrenIds.length !== 0) { - return false; - } - - // First message's parent should be null - const firstMessage = messages[0]; - if (firstMessage.parentId !== null) { - return false; - } - // Every message's content should be a string for (const message of messages) { if (typeof message.content !== 'string') { @@ -799,7 +813,15 @@ export const convertOpenAIChats = (_chats) => { // Create a list of dictionaries with each conversation from import const chats = []; let failed = 0; + let skipped = 0; for (const convo of _chats) { + // Skip folder/project metadata entries that lack a 'mapping' key + if (!('mapping' in convo)) { + skipped++; + console.log('Skipping non-conversation entry (folder/project):', convo['title'] ?? convo['id']); + continue; + } + const chat = convertOpenAIMessages(convo); if (validateChat(chat)) { @@ -815,6 +837,9 @@ export const convertOpenAIChats = (_chats) => { } } console.log(failed, 'Conversations could not be imported'); + if (skipped > 0) { + console.log(skipped, 'Non-conversation entries (folders/projects) were skipped'); + } return chats; };