281 lines
8.9 KiB
JavaScript
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;
|
|
}
|
|
}); |