Files
Claude-Conversation-Exporte…/content.js
T

281 lines
8.9 KiB
JavaScript

// Note: Organization ID is now stored in extension settings
// Users need to configure it in the extension options page
// Fetch conversation data
async function fetchConversation(orgId, conversationId) {
const url = `https://claude.ai/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=True&rendering_mode=messages&render_all_tools=true`;
const response = await fetch(url, {
credentials: 'include',
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`Failed to fetch conversation: ${response.status}`);
}
return await response.json();
}
// Fetch all conversations
async function fetchAllConversations(orgId) {
const url = `https://claude.ai/api/organizations/${orgId}/chat_conversations`;
const response = await fetch(url, {
credentials: 'include',
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`Failed to fetch conversations: ${response.status}`);
}
return await response.json();
}
// Helper function to reconstruct the current branch from the message tree
function getCurrentBranch(data) {
if (!data.chat_messages || !data.current_leaf_message_uuid) {
return [];
}
// Create a map of UUID to message for quick lookup
const messageMap = new Map();
data.chat_messages.forEach(msg => {
messageMap.set(msg.uuid, msg);
});
// Trace back from the current leaf to the root
const branch = [];
let currentUuid = data.current_leaf_message_uuid;
while (currentUuid && messageMap.has(currentUuid)) {
const message = messageMap.get(currentUuid);
branch.unshift(message); // Add to beginning to maintain order
currentUuid = message.parent_message_uuid;
// Stop if we hit the root (parent UUID that doesn't exist in our messages)
if (!messageMap.has(currentUuid)) {
break;
}
}
return branch;
}
// Convert to markdown format
function convertToMarkdown(data, includeMetadata) {
let markdown = `# ${data.name || 'Untitled Conversation'}\n\n`;
if (includeMetadata) {
markdown += `**Created:** ${new Date(data.created_at).toLocaleString()}\n`;
markdown += `**Updated:** ${new Date(data.updated_at).toLocaleString()}\n`;
markdown += `**Model:** ${data.model}\n\n`;
markdown += '---\n\n';
}
// Get only the current branch messages
const branchMessages = getCurrentBranch(data);
for (const message of branchMessages) {
const sender = message.sender === 'human' ? '**You**' : '**Claude**';
markdown += `${sender}:\n\n`;
if (message.content) {
for (const content of message.content) {
if (content.text) {
markdown += `${content.text}\n\n`;
}
}
} else if (message.text) {
markdown += `${message.text}\n\n`;
}
if (includeMetadata && message.created_at) {
markdown += `*${new Date(message.created_at).toLocaleString()}*\n\n`;
}
markdown += '---\n\n';
}
return markdown;
}
// Convert to plain text
function convertToText(data, includeMetadata) {
let text = '';
// Add metadata header if requested
if (includeMetadata) {
text += `${data.name || 'Untitled Conversation'}\n`;
text += `Created: ${new Date(data.created_at).toLocaleString()}\n`;
text += `Updated: ${new Date(data.updated_at).toLocaleString()}\n`;
text += `Model: ${data.model}\n\n`;
text += '---\n\n';
}
// Get only the current branch messages
const branchMessages = getCurrentBranch(data);
// Use simplified format
let humanSeen = false;
let assistantSeen = false;
branchMessages.forEach((message) => {
// Get the message text
let messageText = '';
if (message.content) {
for (const content of message.content) {
if (content.text) {
messageText += content.text;
}
}
} else if (message.text) {
messageText = message.text;
}
// Use full label on first occurrence, then abbreviate
let senderLabel;
if (message.sender === 'human') {
senderLabel = humanSeen ? 'H' : 'Human';
humanSeen = true;
} else {
senderLabel = assistantSeen ? 'A' : 'Assistant';
assistantSeen = true;
}
text += `${senderLabel}: ${messageText}\n\n`;
});
return text.trim();
}
// Download file utility
function downloadFile(content, filename, type = 'application/json') {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'exportConversation') {
console.log('Export conversation request received:', request);
fetchConversation(request.orgId, request.conversationId)
.then(data => {
console.log('Conversation data fetched successfully:', data);
let content, filename, type;
switch (request.format) {
case 'markdown':
content = convertToMarkdown(data, request.includeMetadata);
filename = `claude-conversation-${data.name || request.conversationId}.md`;
type = 'text/markdown';
break;
case 'text':
content = convertToText(data, request.includeMetadata);
filename = `claude-conversation-${data.name || request.conversationId}.txt`;
type = 'text/plain';
break;
default:
content = JSON.stringify(data, null, 2);
filename = `claude-conversation-${data.name || request.conversationId}.json`;
type = 'application/json';
}
console.log('Downloading file:', filename);
downloadFile(content, filename, type);
sendResponse({ success: true });
})
.catch(error => {
console.error('Export conversation error:', error);
sendResponse({
success: false,
error: error.message,
details: error.stack
});
});
return true;
}
if (request.action === 'exportAllConversations') {
console.log('Export all conversations request received:', request);
fetchAllConversations(request.orgId)
.then(async conversations => {
console.log(`Fetched ${conversations.length} conversations`);
if (request.format === 'json') {
// For JSON, export as a single file with all conversations
const filename = `claude-all-conversations-${new Date().toISOString().split('T')[0]}.json`;
console.log('Downloading all conversations as JSON:', filename);
downloadFile(JSON.stringify(conversations, null, 2), filename);
sendResponse({ success: true, count: conversations.length });
} else {
// For other formats, create individual files
let count = 0;
let errors = [];
for (const conv of conversations) {
try {
console.log(`Fetching full conversation ${count + 1}/${conversations.length}: ${conv.uuid}`);
const fullConv = await fetchConversation(request.orgId, conv.uuid);
let content, filename, type;
if (request.format === 'markdown') {
content = convertToMarkdown(fullConv, request.includeMetadata);
filename = `claude-${conv.name || conv.uuid}.md`;
type = 'text/markdown';
} else {
content = convertToText(fullConv, request.includeMetadata);
filename = `claude-${conv.name || conv.uuid}.txt`;
type = 'text/plain';
}
downloadFile(content, filename, type);
count++;
// Add a small delay to avoid overwhelming the API
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`Failed to export conversation ${conv.uuid}:`, error);
errors.push(`${conv.name || conv.uuid}: ${error.message}`);
}
}
if (errors.length > 0) {
console.warn('Some conversations failed to export:', errors);
sendResponse({
success: true,
count,
warnings: `Exported ${count}/${conversations.length} conversations. Some failed: ${errors.join('; ')}`
});
} else {
sendResponse({ success: true, count });
}
}
})
.catch(error => {
console.error('Export all conversations error:', error);
sendResponse({
success: false,
error: error.message,
details: error.stack
});
});
return true;
}
});