بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greasyfork.org/scripts/543706/1639691/Claude%20Mass%20Exporter%20Library.js
// ==UserScript==
// @name Claude Mass Exporter Library
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Mass export library for Claude API Exporter 4.0+
// @author MRL
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// =============================================
// DEPENDENCY CHECK AND UTILITIES
// =============================================
function checkDependency() {
const mainScript = window.claudeExporter;
if (typeof mainScript === 'undefined') {
console.error('[Claude Mass Exporter] Claude API Exporter not found!');
return false;
}
// Initialize utilities after successful dependency check
initializeUtilities();
return true;
}
// Utilities from main script (initialized after main script loads)
let showNotification, sanitizeFileName, formatDate, ArchiveManager;
function initializeUtilities() {
const mainScript = window.claudeExporter;
showNotification = mainScript.showNotification;
sanitizeFileName = mainScript.sanitizeFileName;
formatDate = mainScript.formatDate;
ArchiveManager = mainScript.ArchiveManager;
}
function getOrgId() {
const match = document.cookie.match(/lastActiveOrg=([^;]+)/);
if (!match) throw new Error('Could not find organization ID');
return match[1];
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function formatDateString(dateString) {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString();
}
// =============================================
// CONTEXT AND ROUTING
// =============================================
function getCurrentContext() {
const path = window.location.pathname;
if (path === '/projects') return { type: 'projects' };
if (path.match(/^\/project\/[^/]+$/)) return { type: 'project', projectId: path.split('/')[2] };
if (path === '/recents') return { type: 'recents' };
if (path.match(/^\/chat\/[^/]+$/)) return { type: 'chat' };
return { type: 'unknown' };
}
function getExportChatFolderTemplate(exportType, settings) {
// Determines the correct chat folder template based on export type
switch (exportType) {
case 'projects':
return settings.massExportProjectsChatFolderName;
case 'project':
case 'recents':
return settings.massExportSingleChatFolderName;
default:
return settings.massExportProjectsChatFolderName;
}
}
// =============================================
// SORTING AND PERSISTENCE
// =============================================
function sortItemsByDate(items, sortBy = 'updated', direction = 'desc') {
return [...items].sort((a, b) => {
const dateA = new Date(sortBy === 'created' ? a.created_at : a.updated_at);
const dateB = new Date(sortBy === 'created' ? b.created_at : b.updated_at);
return direction === 'desc' ? dateB - dateA : dateA - dateB;
});
}
function saveSortSettings(sortBy, sortDirection, isProject = false) {
const prefix = isProject ? 'claudeProject' : 'claude';
localStorage.setItem(`${prefix}SortBy`, sortBy);
localStorage.setItem(`${prefix}SortDirection`, sortDirection);
}
function loadSortSettings(isProject = false) {
const prefix = isProject ? 'claudeProject' : 'claude';
return {
sortBy: localStorage.getItem(`${prefix}SortBy`) || 'updated',
sortDirection: localStorage.getItem(`${prefix}SortDirection`) || 'desc'
};
}
// =============================================
// API FUNCTIONS
// =============================================
async function getAllProjects() {
const orgId = getOrgId();
const response = await fetch(`/api/organizations/${orgId}/projects?include_harmony_projects=true&creator_filter=is_creator`);
if (!response.ok) throw new Error(`Failed to fetch projects: ${response.status}`);
return await response.json();
}
async function getProjectConversations(projectUuid) {
const orgId = getOrgId();
const response = await fetch(`/api/organizations/${orgId}/projects/${projectUuid}/conversations`);
if (!response.ok) throw new Error(`Failed to fetch project conversations: ${response.status}`);
return await response.json();
}
async function getAllRecentConversations() {
const orgId = getOrgId();
const response = await fetch(`/api/organizations/${orgId}/chat_conversations?limit=10000`);
if (!response.ok) throw new Error(`Failed to fetch recent conversations: ${response.status}`);
return await response.json();
}
// Use getConversationData from main script
async function getConversationData(conversationId) {
return await window.claudeExporter.getConversationData(conversationId);
}
// =============================================
// SELECTION UI - HTML CREATION
// =============================================
function createSelectionModal(title, isProjectSelection, sortSettings, items) {
return `
<div class="claude-selection-overlay">
<div class="claude-selection-modal">
<div class="claude-selection-header">
<h3>📋 ${title}</h3>
<button class="claude-selection-close" type="button">×</button>
</div>
<div class="claude-selection-content">
${createControlsSection(isProjectSelection, sortSettings)}
<div class="claude-selection-list">
${isProjectSelection ? createProjectList(items) : createSimpleList(items, sortSettings)}
</div>
</div>
<div class="claude-selection-footer">
<button class="claude-btn claude-btn-secondary" type="button" id="cancelSelection">Cancel</button>
<button class="claude-btn claude-btn-primary" type="button" id="exportSelected" disabled>Export Selected</button>
</div>
</div>
</div>
`;
}
function createControlsSection(isProjectSelection, sortSettings) {
const selectAllId = isProjectSelection ? 'selectAllProjects' : 'selectAll';
const selectAllText = isProjectSelection ? 'Select All Projects' : 'Select All';
const sortLabel = isProjectSelection ? 'Sort chats:' : 'Sort by:';
const sortById = isProjectSelection ? 'projectChatSortBy' : 'sortBy';
const sortDirId = isProjectSelection ? 'projectChatSortDirection' : 'sortDirection';
const countText = isProjectSelection ? '0 chats selected' : '0 selected';
return `
<div class="claude-selection-controls">
<button class="claude-btn claude-btn-secondary" type="button" id="${selectAllId}">${selectAllText}</button>
<button class="claude-btn claude-btn-secondary" type="button" id="selectNone">Select None</button>
<div class="claude-sort-controls">
<label class="claude-sort-label">${sortLabel}</label>
<select id="${sortById}" class="claude-sort-select">
<option value="updated" ${sortSettings.sortBy === 'updated' ? 'selected' : ''}>Updated</option>
<option value="created" ${sortSettings.sortBy === 'created' ? 'selected' : ''}>Created</option>
</select>
<select id="${sortDirId}" class="claude-sort-select">
<option value="desc" ${sortSettings.sortDirection === 'desc' ? 'selected' : ''}>Newest first</option>
<option value="asc" ${sortSettings.sortDirection === 'asc' ? 'selected' : ''}>Oldest first</option>
</select>
</div>
<span class="claude-selection-count">${countText}</span>
</div>
`;
}
function createProjectList(items) {
return items.map((project, index) => `
<div class="claude-project-item" data-project-index="${index}">
<div class="claude-project-header">
<button class="claude-project-toggle" type="button" data-project="${index}">
<span class="claude-toggle-icon">▶</span>
</button>
<input type="checkbox" id="project-${index}" class="claude-project-checkbox" data-project="${index}">
<label for="project-${index}" class="claude-project-label">
<div class="claude-project-name">📁 ${project.name}</div>
<div class="claude-project-meta">Updated: ${formatDateString(project.updated_at)} | Created: ${formatDateString(project.created_at)} | <span class="chat-count">Click to load chats</span></div>
</label>
</div>
<div class="claude-project-chats" id="chats-${index}" style="display: none;">
<div class="claude-loading">Loading conversations...</div>
</div>
</div>
`).join('');
}
function createSimpleList(items, sortSettings) {
return sortItemsByDate(items, sortSettings.sortBy, sortSettings.sortDirection).map((item, index) => `
<div class="claude-selection-item">
<input type="checkbox" id="item-${index}" value="${items.indexOf(item)}" class="claude-selection-checkbox">
<label for="item-${index}" class="claude-selection-label">
<div class="claude-selection-name">${item.name}</div>
<div class="claude-selection-meta">Updated: ${formatDateString(item.updated_at)} | Created: ${formatDateString(item.created_at)}${item.project?.name ? ` | ${item.project.name}` : ''}</div>
</label>
</div>
`).join('');
}
function createChatList(conversations, projectIndex) {
return conversations.map((chat, chatIndex) => `
<div class="claude-chat-item">
<input type="checkbox" id="chat-${projectIndex}-${chatIndex}" class="claude-chat-checkbox" data-project="${projectIndex}" data-chat="${chatIndex}">
<label for="chat-${projectIndex}-${chatIndex}" class="claude-chat-label">
<div class="claude-chat-name">💬 ${chat.name}</div>
<div class="claude-chat-meta">Updated: ${formatDateString(chat.updated_at)} | Created: ${formatDateString(chat.created_at)}</div>
</label>
</div>
`).join('');
}
// =============================================
// SELECTION UI - LOGIC
// =============================================
function createSelectionUI(title, items, onExport) {
document.getElementById('claude-selection-ui')?.remove();
const isProjectSelection = title.includes('Projects') && items.length > 0 && items[0].uuid;
const sortSettings = loadSortSettings(isProjectSelection);
const selectionOverlay = document.createElement('div');
selectionOverlay.id = 'claude-selection-ui';
selectionOverlay.innerHTML = createSelectionModal(title, isProjectSelection, sortSettings, items);
document.head.insertAdjacentHTML('beforeend', getSelectionStyles());
document.body.appendChild(selectionOverlay);
if (isProjectSelection) {
setupProjectSelection(items, onExport);
} else {
setupSimpleSelection(items, onExport);
}
setupCommonHandlers();
}
function setupProjectSelection(items, onExport) {
const state = {
selectedChats: new Map(),
projectChats: new Map(),
loadedProjects: new Set()
};
items.forEach((_, index) => state.selectedChats.set(index, new Set()));
const updateUI = () => {
let totalSelectedChats = 0;
state.selectedChats.forEach(chatSet => totalSelectedChats += chatSet.size);
document.querySelector('.claude-selection-count').textContent = `${totalSelectedChats} chats selected`;
document.getElementById('exportSelected').disabled = totalSelectedChats === 0;
// Update project checkboxes
items.forEach((_, projectIndex) => {
const projectCheckbox = document.getElementById(`project-${projectIndex}`);
const chats = state.projectChats.get(projectIndex) || [];
const selectedChatSet = state.selectedChats.get(projectIndex);
if (chats.length === 0) {
// Don't change checked state while loading
if (!projectCheckbox.checked) {
projectCheckbox.indeterminate = false;
}
} else if (selectedChatSet.size === 0) {
projectCheckbox.indeterminate = false;
projectCheckbox.checked = false;
} else if (selectedChatSet.size === chats.length) {
projectCheckbox.indeterminate = false;
projectCheckbox.checked = true;
} else {
projectCheckbox.indeterminate = true;
}
});
};
const loadProjectChats = async (projectIndex, forceReload = false) => {
if (!forceReload && state.loadedProjects.has(projectIndex)) return;
const project = items[projectIndex];
const chatsContainer = document.getElementById(`chats-${projectIndex}`);
try {
const conversations = await getProjectConversations(project.uuid);
// Sort conversations based on current settings
const sortSettings = loadSortSettings(true);
const sortedConversations = sortItemsByDate(conversations, sortSettings.sortBy, sortSettings.sortDirection);
state.projectChats.set(projectIndex, sortedConversations);
state.loadedProjects.add(projectIndex);
// Update count
document.querySelector(`[data-project-index="${projectIndex}"] .chat-count`).textContent = `${sortedConversations.length} chats`;
// Generate chat list
chatsContainer.innerHTML = createChatList(sortedConversations, projectIndex);
// Add chat checkbox handlers
chatsContainer.querySelectorAll('.claude-chat-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const projectIdx = parseInt(e.target.dataset.project);
const chatIdx = parseInt(e.target.dataset.chat);
const selectedChatSet = state.selectedChats.get(projectIdx);
if (e.target.checked) {
selectedChatSet.add(chatIdx);
} else {
selectedChatSet.delete(chatIdx);
}
updateUI();
});
});
} catch (error) {
console.error(`Failed to load chats for project ${project.name}:`, error);
chatsContainer.innerHTML = '<div class="claude-error">❌ Failed to load conversations</div>';
document.querySelector(`[data-project-index="${projectIndex}"] .chat-count`).textContent = 'Error';
}
};
// Setup all event handlers
setupProjectEventHandlers(state, items, updateUI, loadProjectChats, onExport);
updateUI();
}
function setupProjectEventHandlers(state, items, updateUI, loadProjectChats, onExport) {
// Toggle handlers
document.querySelectorAll('.claude-project-toggle').forEach(toggle => {
toggle.addEventListener('click', async (e) => {
e.stopPropagation();
const projectIndex = parseInt(e.currentTarget.dataset.project);
const chatsContainer = document.getElementById(`chats-${projectIndex}`);
const toggleIcon = e.currentTarget.querySelector('.claude-toggle-icon');
if (chatsContainer.style.display === 'none') {
chatsContainer.style.display = 'block';
toggleIcon.classList.add('expanded');
await loadProjectChats(projectIndex, false);
} else {
chatsContainer.style.display = 'none';
toggleIcon.classList.remove('expanded');
}
});
});
// Project checkbox handlers
document.querySelectorAll('.claude-project-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', async (e) => {
const projectIndex = parseInt(e.target.dataset.project);
const selectedChatSet = state.selectedChats.get(projectIndex);
if (e.target.checked) {
// Load chats if needed
if (!state.loadedProjects.has(projectIndex)) {
e.target.disabled = true;
await loadProjectChats(projectIndex, false);
e.target.disabled = false;
}
// Select all chats
const chats = state.projectChats.get(projectIndex) || [];
chats.forEach((_, chatIndex) => {
selectedChatSet.add(chatIndex);
const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`);
if (chatCheckbox) chatCheckbox.checked = true;
});
} else {
// Deselect all chats
selectedChatSet.clear();
const chats = state.projectChats.get(projectIndex) || [];
chats.forEach((_, chatIndex) => {
const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`);
if (chatCheckbox) chatCheckbox.checked = false;
});
}
updateUI();
});
});
// Control buttons
document.getElementById('selectAllProjects').addEventListener('click', async () => {
for (let i = 0; i < items.length; i++) {
const projectCheckbox = document.getElementById(`project-${i}`);
if (!projectCheckbox.checked) {
projectCheckbox.checked = true;
projectCheckbox.dispatchEvent(new Event('change'));
}
}
});
document.getElementById('selectNone').addEventListener('click', () => {
state.selectedChats.forEach((chatSet, projectIndex) => {
chatSet.clear();
document.getElementById(`project-${projectIndex}`).checked = false;
const chats = state.projectChats.get(projectIndex) || [];
chats.forEach((_, chatIndex) => {
const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`);
if (chatCheckbox) chatCheckbox.checked = false;
});
});
updateUI();
});
// Sort handlers
const resortAllProjects = async () => {
for (const projectIndex of state.loadedProjects) {
state.selectedChats.get(projectIndex).clear();
await loadProjectChats(projectIndex, true);
}
updateUI();
};
// Auto-sort when selection changes
document.getElementById('projectChatSortBy').addEventListener('change', (e) => {
saveSortSettings(e.target.value, document.getElementById('projectChatSortDirection').value, true);
resortAllProjects();
});
document.getElementById('projectChatSortDirection').addEventListener('change', (e) => {
saveSortSettings(document.getElementById('projectChatSortBy').value, e.target.value, true);
resortAllProjects();
});
// Export handler
document.getElementById('exportSelected').addEventListener('click', () => {
const selectedData = [];
state.selectedChats.forEach((chatSet, projectIndex) => {
if (chatSet.size > 0) {
const project = items[projectIndex];
const chats = state.projectChats.get(projectIndex) || [];
const selectedProjectChats = Array.from(chatSet).map(chatIndex => chats[chatIndex]);
selectedData.push({ project: project, chats: selectedProjectChats });
}
});
closeModal();
onExport(selectedData);
});
}
function setupSimpleSelection(items, onExport) {
const selectedItems = new Set();
const updateUI = () => {
const count = selectedItems.size;
document.querySelector('.claude-selection-count').textContent = `${count} selected`;
document.getElementById('exportSelected').disabled = count === 0;
};
// Checkbox handlers
document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const index = parseInt(e.target.value);
if (e.target.checked) {
selectedItems.add(index);
} else {
selectedItems.delete(index);
}
updateUI();
});
});
// Control buttons
document.getElementById('selectAll').addEventListener('click', () => {
document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => {
checkbox.checked = true;
selectedItems.add(parseInt(checkbox.value));
});
updateUI();
});
document.getElementById('selectNone').addEventListener('click', () => {
document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
selectedItems.clear();
updateUI();
});
// Sort handlers
const resortList = () => {
const sortBy = document.getElementById('sortBy').value;
const sortDirection = document.getElementById('sortDirection').value;
const sortedItems = sortItemsByDate(items, sortBy, sortDirection);
const listContainer = document.querySelector('.claude-selection-list');
listContainer.innerHTML = sortedItems.map((item, index) => `
<div class="claude-selection-item">
<input type="checkbox" id="item-${index}" value="${items.indexOf(item)}" class="claude-selection-checkbox">
<label for="item-${index}" class="claude-selection-label">
<div class="claude-selection-name">${item.name}</div>
<div class="claude-selection-meta">Updated: ${formatDateString(item.updated_at)} | Created: ${formatDateString(item.created_at)}${item.project?.name ? ` | ${item.project.name}` : ''}</div>
</label>
</div>
`).join('');
// Re-setup checkbox handlers
document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const index = parseInt(e.target.value);
if (e.target.checked) {
selectedItems.add(index);
} else {
selectedItems.delete(index);
}
updateUI();
});
});
};
// Auto-sort when selection changes
document.getElementById('sortBy').addEventListener('change', (e) => {
saveSortSettings(e.target.value, document.getElementById('sortDirection').value, false);
resortList();
});
document.getElementById('sortDirection').addEventListener('change', (e) => {
saveSortSettings(document.getElementById('sortBy').value, e.target.value, false);
resortList();
});
// Export handler
document.getElementById('exportSelected').addEventListener('click', () => {
const selected = Array.from(selectedItems).map(index => items[index]);
closeModal();
onExport(selected);
});
updateUI();
}
function setupCommonHandlers() {
document.querySelector('.claude-selection-close').addEventListener('click', closeModal);
document.getElementById('cancelSelection').addEventListener('click', closeModal);
document.querySelector('.claude-selection-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('claude-selection-overlay')) closeModal();
});
}
function closeModal() {
document.getElementById('claude-selection-ui')?.remove();
document.getElementById('claude-selection-styles')?.remove();
}
// =============================================
// PROGRESS UI
// =============================================
function createProgressUI(title) {
document.getElementById('claude-mass-export-progress')?.remove();
const progressOverlay = document.createElement('div');
progressOverlay.id = 'claude-mass-export-progress';
progressOverlay.innerHTML = `
<div class="claude-progress-overlay">
<div class="claude-progress-modal">
<div class="claude-progress-header">
<h3>📦 ${title}</h3>
<button class="claude-progress-close" type="button">×</button>
</div>
<div class="claude-progress-content">
<div class="claude-progress-bar">
<div class="claude-progress-fill" style="width: 0%"></div>
</div>
<div class="claude-progress-text">Initializing...</div>
<div class="claude-progress-details"></div>
</div>
</div>
</div>
`;
document.head.insertAdjacentHTML('beforeend', getProgressStyles());
document.body.appendChild(progressOverlay);
let cancelled = false;
const closeProgressModal = () => {
cancelled = true;
document.getElementById('claude-mass-export-progress')?.remove();
document.getElementById('claude-progress-styles')?.remove();
};
document.querySelector('.claude-progress-close').addEventListener('click', closeProgressModal);
document.querySelector('.claude-progress-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('claude-progress-overlay')) closeProgressModal();
});
return {
updateProgress: (current, total, text, details = '') => {
if (cancelled) return false;
const percentage = Math.round((current / total) * 100);
document.querySelector('.claude-progress-fill').style.width = `${percentage}%`;
document.querySelector('.claude-progress-text').textContent = text;
document.querySelector('.claude-progress-details').textContent = details;
if (current === total) {
setTimeout(closeProgressModal, 2000);
}
return true;
},
isCancelled: () => cancelled,
close: closeProgressModal
};
}
// =============================================
// EXPORT ORCHESTRATION
// =============================================
async function exportSingleConversation(conversationData, exportMode = 'final', archiveManager = null, projectFolderName = '', useChatFolders = false, exportType = 'projects', mainBranchOnly = false) {
const mainScript = window.claudeExporter;
if (!mainScript) throw new Error('Main exporter not available');
// Determine folder structure
const settings = mainScript.loadSettings();
let finalFolderPath = '';
if (archiveManager && projectFolderName) {
// Mass export with project folders (only for exporting all projects)
if (useChatFolders) {
// Project/Chat/ structure
const template = getExportChatFolderTemplate('projects', settings);
const chatFolderName = mainScript.generateChatFolderName(
conversationData,
{ name: projectFolderName },
template
);
finalFolderPath = chatFolderName;
} else {
// Project/ structure (files directly in project folder)
finalFolderPath = projectFolderName;
}
} else if (archiveManager && useChatFolders) {
// Single export with chat folders (for a separate project or recent ones)
const template = getExportChatFolderTemplate(exportType, settings);
finalFolderPath = mainScript.generateChatFolderName(conversationData, null, template);
}
// Extract and process artifacts from all branches - use main script's export logic
const { branchArtifacts, branchInfo, mainBranchUuids } = mainScript.extractAllArtifacts(conversationData);
// Filter data based on export mode and main branch settings
let filteredConversationData = conversationData;
let filteredBranchArtifacts = branchArtifacts;
const needMainBranchFiltering = (mainBranchOnly && !settings.mainBranchOnlyIncludeAllMessages) ||
(exportMode === 'none' && settings.conversationOnlyArtifactMode === 'main_branch_only');
if (needMainBranchFiltering) {
filteredConversationData = {
...conversationData,
name: conversationData.name,
chat_messages: conversationData.chat_messages.filter(message =>
mainBranchUuids && mainBranchUuids.has(message.uuid)
)
};
}
// Filter artifacts if main branch only
if (mainBranchOnly) {
const filteredMap = new Map();
// Only include artifacts from messages in main branch
for (const [branchId, artifactsMap] of branchArtifacts) {
const filteredArtifactsMap = new Map();
for (const [artifactId, versions] of artifactsMap) {
// Filter versions to only those from main branch messages
const mainVersions = versions.filter(version =>
mainBranchUuids && mainBranchUuids.has(version.messageUuid)
);
if (mainVersions.length > 0) {
filteredArtifactsMap.set(artifactId, mainVersions);
}
}
if (filteredArtifactsMap.size > 0) {
filteredMap.set(branchId, filteredArtifactsMap);
}
}
filteredBranchArtifacts = filteredMap;
}
// For 'none' mode (conversation only), use conversationOnlyArtifactMode setting - handle conversation-only export
if (exportMode === 'none') {
let conversationOnlyMode = settings.conversationOnlyArtifactMode;
let conversationOnlyBranchArtifacts = filteredBranchArtifacts;
let conversationOnlyData = filteredConversationData;
// Filter artifacts for main_branch_only mode
if (conversationOnlyMode === 'main_branch_only') {
conversationOnlyMode = 'all';
conversationOnlyBranchArtifacts = new Map();
// Only include artifacts from messages in main branch
for (const [branchId, artifactsMap] of branchArtifacts) {
const filteredArtifactsMap = new Map();
for (const [artifactId, versions] of artifactsMap) {
// Filter versions to only those from main branch messages
const mainVersions = versions.filter(version =>
mainBranchUuids && mainBranchUuids.has(version.messageUuid)
);
if (mainVersions.length > 0) {
filteredArtifactsMap.set(artifactId, mainVersions);
}
}
if (filteredArtifactsMap.size > 0) {
conversationOnlyBranchArtifacts.set(branchId, filteredArtifactsMap);
}
}
// Use full conversation data if mainBranchOnlyIncludeAllMessages is true
if (settings.mainBranchOnlyIncludeAllMessages) {
conversationOnlyData = conversationData;
}
}
const conversationMarkdown = mainScript.generateConversationMarkdown(conversationOnlyData, conversationOnlyMode, conversationOnlyBranchArtifacts, branchInfo, mainBranchUuids);
let filename = mainScript.generateConversationFilename(conversationData);
// Add full path for archive
if (finalFolderPath) {
filename = `${finalFolderPath}/${filename}`;
}
if (archiveManager) {
// Use full path in filename, no additional folder processing
await archiveManager.addFile(filename, conversationMarkdown, false, '');
} else {
mainScript.downloadFile(filename, conversationMarkdown);
}
return 1;
}
// Determine include mode for conversation markdown - regular export with artifacts - use main script logic
let includeMode;
if (mainBranchOnly) {
includeMode = 'all';
} else if (exportMode === 'latest_per_message') {
includeMode = 'latest_per_message';
} else if (exportMode === 'final') {
includeMode = 'final';
} else {
includeMode = 'all';
}
let conversationMarkdown, shouldExportSeparateFiles = false;
// Determine behavior based on setting
switch (settings.artifactExportMode) {
case 'embed':
conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, includeMode, filteredBranchArtifacts, branchInfo, mainBranchUuids);
break;
case 'files':
conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, 'none', filteredBranchArtifacts, branchInfo, mainBranchUuids);
shouldExportSeparateFiles = true;
break;
case 'both':
conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, includeMode, filteredBranchArtifacts, branchInfo, mainBranchUuids);
shouldExportSeparateFiles = true;
break;
}
// Generate filename with full path for archive
let finalFilename = mainScript.generateConversationFilename(conversationData);
if (finalFolderPath) {
finalFilename = `${finalFolderPath}/${finalFilename}`;
}
// Add conversation file
if (archiveManager) {
// Use full path in filename, no additional folder processing
await archiveManager.addFile(finalFilename, conversationMarkdown, false, '');
} else {
mainScript.downloadFile(finalFilename, conversationMarkdown);
}
let exportedCount = 1; // Conversation file
// Export artifacts if needed
if (shouldExportSeparateFiles && filteredBranchArtifacts.size > 0) {
// For latest per message mode, build set of latest artifact timestamps
let latestArtifactTimestamps = new Set();
if (exportMode === 'latest_per_message') {
filteredConversationData.chat_messages.forEach(message => {
const latestInMessage = new Map();
message.content.forEach(content => {
if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
latestInMessage.set(content.input.id, content);
}
});
latestInMessage.forEach((content) => {
if (content.stop_timestamp) {
latestArtifactTimestamps.add(content.stop_timestamp);
}
});
});
}
for (const [branchId, artifactsMap] of filteredBranchArtifacts) {
const branchData = branchInfo.find(b => b.branchId === branchId);
const branchLabel = branchData ? branchData.branchIndex.toString() : 'unknown';
for (const [artifactId, versions] of artifactsMap) {
let versionsToExport = versions;
if (exportMode === 'latest_per_message') {
versionsToExport = versions.filter(version => latestArtifactTimestamps.has(version.content_stop_timestamp));
} else if (exportMode === 'final' && !mainBranchOnly) {
versionsToExport = [versions[versions.length - 1]];
}
for (const version of versionsToExport) {
if (settings.excludeCanceledArtifacts && version.stop_reason === 'user_canceled') continue;
const correctIsMain = mainBranchUuids && mainBranchUuids.has(version.messageUuid);
const artifactFilename = mainScript.generateArtifactFilename(version, conversationData, branchLabel, correctIsMain, artifactId);
// Add full path for archive
let fullArtifactFilename = artifactFilename;
if (finalFolderPath) {
fullArtifactFilename = `${finalFolderPath}/${artifactFilename}`;
}
const metadata = mainScript.formatArtifactMetadata(version, artifactId, branchLabel, correctIsMain);
let processedContent = version.fullContent;
if (version.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) {
processedContent = mainScript.processArtifactContent(version.fullContent, version.finalType, true);
}
const content = metadata ? metadata + '\n' + processedContent : processedContent;
if (archiveManager) {
// Use full path in filename, no additional folder processing
await archiveManager.addFile(fullArtifactFilename, content, false, '');
} else {
mainScript.downloadFile(fullArtifactFilename, content);
}
exportedCount++;
}
}
}
}
return exportedCount;
}
async function performMassExport(selectedData, exportMode, mainBranchOnly, exportType, progressTitle) {
const progress = createProgressUI(progressTitle);
try {
const mainScript = window.claudeExporter;
const settings = mainScript.loadSettings();
// Determine if we should use archive
const useArchive = settings.forceArchiveForMassExport;
const useChatFolders = settings.forceChatFoldersForMassExport;
let archiveManager = null;
if (useArchive) {
archiveManager = new ArchiveManager();
await archiveManager.initialize();
}
let totalConversations = 0;
if (exportType === 'projects') {
totalConversations = selectedData.reduce((total, projectData) => total + projectData.chats.length, 0);
} else {
totalConversations = selectedData.length;
}
if (totalConversations === 0) {
showNotification('No conversations selected for export', 'info');
progress.close();
return;
}
// Export conversations
let currentConversation = 0;
let totalExported = 0;
if (exportType === 'projects') {
for (const projectData of selectedData) {
if (progress.isCancelled()) return;
const project = projectData.project;
const chats = projectData.chats;
const projectFolderName = sanitizeFileName(project.name);
for (const chat of chats) {
if (progress.isCancelled()) return;
currentConversation++;
progress.updateProgress(currentConversation, totalConversations,
`Exporting conversation ${currentConversation}/${totalConversations}`,
`Project: ${project.name} | Chat: ${chat.name}`);
try {
const fullConversationData = await getConversationData(chat.uuid);
const exportedCount = await exportSingleConversation(fullConversationData, exportMode, archiveManager, projectFolderName, useChatFolders, 'projects', mainBranchOnly);
totalExported += exportedCount;
} catch (error) {
console.warn(`Failed to export conversation ${chat.name}:`, error);
}
await delay(200);
}
}
} else {
for (const conversation of selectedData) {
if (progress.isCancelled()) return;
currentConversation++;
progress.updateProgress(currentConversation, totalConversations,
`Exporting conversation ${currentConversation}/${totalConversations}`,
`Chat: ${conversation.name}`);
try {
const fullConversationData = await getConversationData(conversation.uuid);
const exportedCount = await exportSingleConversation(fullConversationData, exportMode, archiveManager, '', useChatFolders, exportType, mainBranchOnly);
totalExported += exportedCount;
} catch (error) {
console.warn(`Failed to export conversation ${conversation.name}:`, error);
}
await delay(200);
}
}
if (archiveManager && archiveManager.fileCount > 0) {
let exportName;
if (exportType === 'projects') {
exportName = `${selectedData.length} Projects Export`;
} else {
exportName = `${selectedData.length} Conversations Export`;
}
const archiveName = mainScript.generateArchiveName({
name: exportName,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
uuid: 'mass-export'
}, settings.massExportArchiveName, true, exportType === 'projects' ? 'Projects' : 'Conversations');
await archiveManager.downloadArchive(archiveName);
}
if (exportType === 'projects') {
showNotification(`✅ Mass export completed! Downloaded ${totalExported} files from ${totalConversations} conversations across ${selectedData.length} projects`, 'success');
} else {
showNotification(`✅ Export completed! Downloaded ${totalExported} files from ${selectedData.length} conversations`, 'success');
}
} catch (error) {
console.error('Mass export failed:', error);
showNotification(`❌ Mass export failed: ${error.message}`, 'error');
}
}
// =============================================
// MAIN EXPORT FUNCTIONS
// =============================================
async function exportAllProjects(exportMode = 'final', mainBranchOnly = false) {
try {
showNotification('Fetching projects...', 'info');
const projects = await getAllProjects();
if (projects.length === 0) {
showNotification('No projects found to export', 'info');
return;
}
const projectItems = projects.map(project => ({ ...project, name: project.name }));
createSelectionUI('Select Projects to Export', projectItems, async (selectedData) => {
const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode);
await performMassExport(selectedData, exportMode, mainBranchOnly, 'projects', `Mass Export - Selected Projects (${modeText})`);
});
} catch (error) {
console.error('Failed to fetch projects:', error);
showNotification(`❌ Failed to fetch projects: ${error.message}`, 'error');
}
}
async function exportCurrentProject(exportMode = 'final', mainBranchOnly = false) {
const context = getCurrentContext();
if (context.type !== 'project') {
showNotification('❌ Not in a project page. Please navigate to a project first.', 'error');
return;
}
try {
showNotification('Fetching project conversations...', 'info');
const conversations = await getProjectConversations(context.projectId);
if (conversations.length === 0) {
showNotification('No conversations found in this project', 'info');
return;
}
// Sort conversations using saved settings
const sortSettings = loadSortSettings(false);
const sortedConversations = sortItemsByDate(conversations, sortSettings.sortBy, sortSettings.sortDirection);
// Show selection UI
const conversationItems = sortedConversations.map(conv => ({ ...conv, name: conv.name }));
createSelectionUI('Select Conversations to Export', conversationItems, async (selectedConversations) => {
const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode);
await performMassExport(selectedConversations, exportMode, mainBranchOnly, 'project', `Export Selected Conversations (${modeText})`);
});
} catch (error) {
console.error('Failed to fetch conversations:', error);
showNotification(`❌ Failed to fetch conversations: ${error.message}`, 'error');
}
}
async function exportAllRecentConversations(exportMode = 'final', mainBranchOnly = false) {
try {
showNotification('Fetching recent conversations...', 'info');
const conversations = await getAllRecentConversations();
if (conversations.length === 0) {
showNotification('No recent conversations found', 'info');
return;
}
const conversationItems = conversations.map(conv => ({ ...conv, name: conv.name }));
createSelectionUI('Select Recent Conversations to Export', conversationItems, async (selectedConversations) => {
const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode);
await performMassExport(selectedConversations, exportMode, mainBranchOnly, 'recents', `Export Selected Recent Conversations (${modeText})`);
});
} catch (error) {
console.error('Failed to fetch conversations:', error);
showNotification(`❌ Failed to fetch conversations: ${error.message}`, 'error');
}
}
// =============================================
// STYLES (MINIMAL - ONLY SELECTION SPECIFIC)
// =============================================
function getSelectionStyles() {
return `<style id="claude-selection-styles">
.claude-selection-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif}
.claude-selection-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:700px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column}
.claude-selection-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between}
.claude-selection-header h3{margin:0;font-size:18px;font-weight:600}
.claude-selection-close{background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color 0.2s}
.claude-selection-close:hover{background:rgba(255,255,255,0.2)}
.claude-selection-content{flex:1;overflow-y:auto;padding:24px}
.claude-selection-controls{display:flex;gap:8px;align-items:center;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}
.claude-selection-count{margin-left:auto;font-size:14px;color:#718096;font-weight:500}
.claude-sort-controls{display:flex;gap:6px;align-items:center;margin-left:16px;padding-left:16px;border-left:1px solid #e2e8f0}
.claude-sort-label{font-size:12px;color:#718096;font-weight:500}
.claude-sort-select{padding:4px 8px;border:1px solid #e2e8f0;border-radius:4px;font-size:12px;background:white}
.claude-sort-select:focus{outline:none;border-color:#667eea}
.claude-selection-list{display:grid;gap:8px}
.claude-selection-item{display:flex;align-items:flex-start;gap:12px;padding:12px;border:1px solid #e2e8f0;border-radius:8px;transition:all 0.2s}
.claude-selection-item:hover{background:#f8fafc;border-color:#667eea}
.claude-selection-checkbox{margin-top:2px;transform:scale(1.2)}
.claude-selection-label{flex:1;cursor:pointer;line-height:1.4}
.claude-selection-name{font-weight:500;color:#2d3748;margin-bottom:4px}
.claude-selection-meta{font-size:13px;color:#718096}
.claude-project-item{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;transition:all 0.2s}
.claude-project-item:hover{border-color:#667eea}
.claude-project-header{display:flex;align-items:center;gap:8px;padding:12px;background:#f8fafc}
.claude-project-toggle{background:none;border:none;color:#718096;cursor:pointer;font-size:12px;padding:4px;border-radius:4px;transition:all 0.2s;min-width:20px}
.claude-project-toggle:hover{background:#e2e8f0}
.claude-toggle-icon{transition:transform 0.2s;display:inline-block}
.claude-toggle-icon.expanded{transform:rotate(90deg)}
.claude-project-checkbox{transform:scale(1.2)}
.claude-project-label{flex:1;cursor:pointer;line-height:1.4}
.claude-project-name{font-weight:600;color:#2d3748;margin-bottom:4px}
.claude-project-meta{font-size:13px;color:#718096}
.chat-count{font-weight:500;color:#667eea}
.claude-project-chats{background:#fff;border-top:1px solid #e2e8f0}
.claude-chat-item{display:flex;align-items:center;gap:12px;padding:8px 16px 8px 48px;transition:all 0.2s;border-bottom:1px solid #f1f5f9}
.claude-chat-item:last-child{border-bottom:none}
.claude-chat-item:hover{background:#f8fafc}
.claude-chat-checkbox{transform:scale(1.1)}
.claude-chat-label{flex:1;cursor:pointer;line-height:1.3}
.claude-chat-name{font-weight:500;color:#374151;margin-bottom:2px}
.claude-chat-meta{font-size:12px;color:#9ca3af}
.claude-loading{padding:16px;text-align:center;color:#718096;font-style:italic}
.claude-error{padding:16px;text-align:center;color:#ef4444;font-size:13px}
.claude-btn{padding:8px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all 0.2s}
.claude-btn-primary{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white}
.claude-btn-primary:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,0.4)}
.claude-btn-primary:disabled{opacity:0.5;cursor:not-allowed}
.claude-btn-secondary{background:#e2e8f0;color:#2d3748;font-size:12px;padding:6px 12px}
.claude-btn-secondary:hover{background:#cbd5e0}
.claude-selection-footer{background:#f8fafc;padding:20px 24px;border-top:1px solid #e2e8f0;display:flex;gap:12px;justify-content:flex-end}
@media (prefers-color-scheme: dark){.claude-sort-select{color:#1f2937!important}.claude-sort-select option{color:#1f2937!important}}
</style>`;
}
function getProgressStyles() {
return `<style id="claude-progress-styles">
.claude-progress-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif}
.claude-progress-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:500px;overflow:hidden}
.claude-progress-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between}
.claude-progress-header h3{margin:0;font-size:18px;font-weight:600}
.claude-progress-close{background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color 0.2s}
.claude-progress-close:hover{background:rgba(255,255,255,0.2)}
.claude-progress-content{padding:24px}
.claude-progress-bar{width:100%;height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden;margin-bottom:16px}
.claude-progress-fill{height:100%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);transition:width 0.3s ease;border-radius:4px}
.claude-progress-text{font-size:16px;font-weight:500;color:#2d3748;margin-bottom:8px}
.claude-progress-details{font-size:14px;color:#718096;line-height:1.5;min-height:20px}
</style>`;
}
// =============================================
// INITIALIZATION
// =============================================
async function waitForMainScript() {
return new Promise((resolve) => {
let attempts = 0;
const maxAttempts = 50;
const checkInterval = setInterval(() => {
attempts++;
if (typeof window.claudeExporter !== 'undefined') {
console.log(`[Claude Mass Exporter] Main script found after ${attempts} attempts`);
clearInterval(checkInterval);
resolve(true);
}
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
resolve(false);
}
}, 100);
});
}
async function init() {
console.log('[Claude Mass Exporter] Initializing...');
const mainScriptLoaded = await waitForMainScript();
if (!mainScriptLoaded) {
console.error('[Claude Mass Exporter] Main script not detected after 5 seconds');
return;
}
if (!checkDependency()) return;
console.log('[Claude Mass Exporter] Main script detected, exposing mass export functions...');
window.claudeMassExporter = {
exportAllProjects,
exportCurrentProject,
exportAllRecentConversations,
};
console.log('[Claude Mass Exporter] Enhanced export functionality activated!');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();