// ==UserScript==
// @name Claude API Exporter
// @namespace http://tampermonkey.net/
// @version 4.5.1
// @description Export Claude conversations, artifacts and projects
// @author MRL
// @match https://claude.ai/*
// @grant GM_registerMenuCommand
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAJFBMVEVHcEzZd1fZd1fZd1fZd1fad1jZd1fZd1fZd1fZd1fZd1fZd1deZDooAAAADHRSTlMA//F3mhLfyjpWsiMDGU5mAAABH0lEQVQokXVSWXbEIAzDuw33v28FJCnpTP3BA6+yRGvbLK39a0F9RUvHZ5CIazZwkm+VpCi1nZP1GpJEnq2NdabzU594N0XpzInRhq/7jvm8wsOjFfVmcQG4guTWZKYL8HSiA1XFHDjgHMqCpKfpNPgIXiZVVvSJ9yazOJARxBiYf/ZMoIUxrYGjRHs/di2nbdzDZxIb1maPriold3QlyFJCAonMd08A7/WhkN19PWDolSeiXU7URWPm8S3ewMCuvGpB+kigVXvWZKlgIVycF0Hjl6DIoVRCGKz9pE8W0QKXkgkIEmjzUDuh11TSuVkn348DLHs1Y7/kzTi+ksX8GLn0wBRrdvCQS949C43fstj6FhfM8Unfqqwv3gf3+/kDMJgHC0kwnjEAAAAASUVORK5CYII=
// @license MIT
// @require https://update.greasyfork.icu/scripts/543706/Claude%20Mass%20Exporter%20Library.js
// @require https://update.greasyfork.icu/scripts/546704/Claude%20Project%20Documents%20Exporter%20Library.js
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// =============================================
// CORE & UTILITIES
// =============================================
// JSZIP DYNAMIC LOADER
// Code from https://greasyfork.org/ru/scripts/541467
function loadJSZip() {
return new Promise((resolve, reject) => {
if (typeof JSZip !== 'undefined') {
// console.log('[Claude API Exporter] JSZip already available');
resolve();
return;
}
// console.log('[Claude API Exporter] Loading JSZip from CDN...');
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
script.onload = () => {
// console.log('[Claude API Exporter] JSZip script loaded');
setTimeout(() => {
if (typeof JSZip !== 'undefined') {
// console.log('[Claude API Exporter] JSZip is now available');
resolve();
} else {
reject(new Error('JSZip loaded but not available'));
}
}, 500);
};
script.onerror = () => {
console.error('[Claude API Exporter] Failed to load JSZip');
reject(new Error('Failed to load JSZip'));
};
document.head.appendChild(script);
});
}
// CONSTANTS AND DEFAULT SETTINGS
/**
* Default settings configuration
*/
const defaultSettings = {
conversationTemplate: '[{created_date}_{created_time}] {conversation_title}.md',
artifactTemplate: '[{timestamp}] {conversation_title} - Branch{branch}{main_suffix}_v{version}_{artifact_title}_{artifactId}_{command}{extension}',
dateFormat: 'YYYY-MM-DD_HH-MM-SS', // YYYYMMDDHHMMSS, YYYY-MM-DD_HH-MM-SS, ISO
artifactExportMode: 'files', // Flexible artifact export settings: 'embed', 'files', 'both'
conversationOnlyArtifactMode: 'none', // 'none', 'final', 'all', 'latest_per_message'
includeArtifactMetadata: true, // Include metadata comments in artifact files
excludeCanceledArtifacts: true, // User canceled message
// Content formatting settings
excludeAttachments: false, // Exclude attachments from conversation export
removeDoubleNewlinesFromConversation: false, // Remove \n\n from conversation content
removeDoubleNewlinesFromMarkdown: false, // Remove \n\n from markdown artifact content
includeParentMessageUuid: false, // Include parent message UUID in conversation export
conversationSortMode: 'chronological', // 'chronological', 'logical', or 'pairs'
// Archive settings
exportToArchive: true, // Export files to ZIP archive
createChatFolders: false, // Create folders for each chat in archive
archiveName: 'Claude_Export_{timestamp}_{conversation_title}',
chatFolderName: '[{created_date}_{created_time}] {conversation_title}',
// Mass export archive settings (only when mass exporter detected)
forceArchiveForMassExport: true, // Force archive for mass exports
forceChatFoldersForMassExport: true, // Force chat folders for mass exports
massExportArchiveName: 'Claude_{export_type}_Export_{timestamp}',
massExportProjectsChatFolderName: '{project_name}/[{created_date}_{created_time}] {conversation_title}', // Export all projects
massExportSingleChatFolderName: '[{created_date}_{created_time}] {conversation_title}', // Export a single project and recent conversations
// Main branch export settings
useCurrentLeafForMainBranch: false, // Use current_leaf_message_uuid instead of max index
exportOnlyCurrentBranch: false, // Use tree=false to get only user-selected branch
mainBranchOnlyIncludeAllMessages: false, // Include all messages in main branch export
// Export menu button settings
enableConversationOnly: true,
enableFinalArtifacts: true,
enableAllArtifacts: true,
enableLatestPerMessage: true,
enableMainBranchOnly: true,
enableRawApiExport: false,
rawApiExportFormat: 'pretty', // 'pretty', 'compact'
};
/**
* Available variables for filename templates
*/
const availableVariables = {
// Common variables
'{timestamp}': 'Main timestamp (conversation: updated_at, artifact: content_stop_timestamp) in dateFormat setting',
'{conversationId}': 'Unique conversation identifier',
'{conversation_title}': 'Conversation title',
'{artifact_title}': 'Artifact title',
'{export_type}': 'Type of export (Conversation, Projects, Recent, etc.)',
'{project_name}': 'Project name (for mass exports)',
// Artifact-specific variables
'{artifactId}': 'Unique artifact identifier',
'{version}': 'Artifact version number',
'{branch}': 'Branch number (e.g., 1, 2, 3)',
'{main_suffix}': 'Suffix "_main" for main branch, empty for others',
'{command}': 'Artifact command (create, update, rewrite)',
'{extension}': 'File extension based on artifact type',
// Conversation created_at timestamps
'{created_date}': 'Conversation created_at date in YYYY-MM-DD format',
'{created_time}': 'Conversation created_at time in HH-MM-SS format',
'{created_year}': 'Conversation created_at year (YYYY)',
'{created_month}': 'Conversation created_at month (MM)',
'{created_day}': 'Conversation created_at day (DD)',
'{created_hour}': 'Conversation created_at hour (HH)',
'{created_minute}': 'Conversation created_at minute (MM)',
'{created_second}': 'Conversation created_at second (SS)',
// Conversation updated_at timestamps
'{updated_date}': 'Conversation updated_at date in YYYY-MM-DD format',
'{updated_time}': 'Conversation updated_at time in HH-MM-SS format',
'{updated_year}': 'Conversation updated_at year (YYYY)',
'{updated_month}': 'Conversation updated_at month (MM)',
'{updated_day}': 'Conversation updated_at day (DD)',
'{updated_hour}': 'Conversation updated_at hour (HH)',
'{updated_minute}': 'Conversation updated_at minute (MM)',
'{updated_second}': 'Conversation updated_at second (SS)',
// Artifact content_start_timestamp
'{content_start_date}': 'Artifact content_start_timestamp date in YYYY-MM-DD format',
'{content_start_time}': 'Artifact content_start_timestamp time in HH-MM-SS format',
'{content_start_year}': 'Artifact content_start_timestamp year (YYYY)',
'{content_start_month}': 'Artifact content_start_timestamp month (MM)',
'{content_start_day}': 'Artifact content_start_timestamp day (DD)',
'{content_start_hour}': 'Artifact content_start_timestamp hour (HH)',
'{content_start_minute}': 'Artifact content_start_timestamp minute (MM)',
'{content_start_second}': 'Artifact content_start_timestamp second (SS)',
// Artifact content_stop_timestamp
'{content_stop_date}': 'Artifact content_stop_timestamp date in YYYY-MM-DD format',
'{content_stop_time}': 'Artifact content_stop_timestamp time in HH-MM-SS format',
'{content_stop_year}': 'Artifact content_stop_timestamp year (YYYY)',
'{content_stop_month}': 'Artifact content_stop_timestamp month (MM)',
'{content_stop_day}': 'Artifact content_stop_timestamp day (DD)',
'{content_stop_hour}': 'Artifact content_stop_timestamp hour (HH)',
'{content_stop_minute}': 'Artifact content_stop_timestamp minute (MM)',
'{content_stop_second}': 'Artifact content_stop_timestamp second (SS)',
// Current export time variables
'{current_date}': 'Current export date in YYYY-MM-DD format',
'{current_time}': 'Current export time in HH-MM-SS format',
'{current_year}': 'Current export year (YYYY)',
'{current_month}': 'Current export month (MM)',
'{current_day}': 'Current export day (DD)',
'{current_hour}': 'Current export hour (HH)',
'{current_minute}': 'Current export minute (MM)',
'{current_second}': 'Current export second (SS)'
};
// =============================================
// SETTINGS MANAGEMENT
// =============================================
/**
* Load settings from storage
*/
function loadSettings() {
const settings = {};
for (const [key, defaultValue] of Object.entries(defaultSettings)) {
settings[key] = GM_getValue(key, defaultValue);
}
return settings;
}
/**
* Save settings to storage
*/
function saveSettings(settings) {
for (const [key, value] of Object.entries(settings)) {
GM_setValue(key, value);
}
}
/**
* Apply variables to template string
*/
function applyTemplate(template, variables) {
let result = template;
for (const [placeholder, value] of Object.entries(variables)) {
// Replace all occurrences of the placeholder
result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value || '');
}
return result;
}
// =============================================
// SETTINGS SYSTEM
// =============================================
/**
* Creates standard template variables from conversation data
*/
function createStandardTemplateVariables(conversationData) {
const createdDate = new Date(conversationData.created_at);
const updatedDate = new Date(conversationData.updated_at);
const currentDate = new Date();
return {
'{conversationId}': conversationData.uuid,
'{conversation_title}': sanitizeFileName(conversationData.name),
// Conversation created_at variables
'{created_date}': createdDate.toISOString().split('T')[0],
'{created_time}': createdDate.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{created_year}': createdDate.getFullYear().toString(),
'{created_month}': String(createdDate.getMonth() + 1).padStart(2, '0'),
'{created_day}': String(createdDate.getDate()).padStart(2, '0'),
'{created_hour}': String(createdDate.getHours()).padStart(2, '0'),
'{created_minute}': String(createdDate.getMinutes()).padStart(2, '0'),
'{created_second}': String(createdDate.getSeconds()).padStart(2, '0'),
// Conversation updated_at variables
'{updated_date}': updatedDate.toISOString().split('T')[0],
'{updated_time}': updatedDate.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{updated_year}': updatedDate.getFullYear().toString(),
'{updated_month}': String(updatedDate.getMonth() + 1).padStart(2, '0'),
'{updated_day}': String(updatedDate.getDate()).padStart(2, '0'),
'{updated_hour}': String(updatedDate.getHours()).padStart(2, '0'),
'{updated_minute}': String(updatedDate.getMinutes()).padStart(2, '0'),
'{updated_second}': String(updatedDate.getSeconds()).padStart(2, '0'),
// Current time variables
'{current_date}': currentDate.toISOString().split('T')[0],
'{current_time}': currentDate.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{current_year}': currentDate.getFullYear().toString(),
'{current_month}': String(currentDate.getMonth() + 1).padStart(2, '0'),
'{current_day}': String(currentDate.getDate()).padStart(2, '0'),
'{current_hour}': String(currentDate.getHours()).padStart(2, '0'),
'{current_minute}': String(currentDate.getMinutes()).padStart(2, '0'),
'{current_second}': String(currentDate.getSeconds()).padStart(2, '0')
};
}
/**
* Creates artifact-specific template variables
*/
function createArtifactTemplateVariables(version, conversationData, branchLabel, isMain, artifactId) {
const baseVariables = createStandardTemplateVariables(conversationData);
const contentStartDate = new Date(version.content_start_timestamp);
const contentStopDate = new Date(version.content_stop_timestamp);
return {
...baseVariables,
'{timestamp}': generateTimestamp(version.content_stop_timestamp),
'{artifactId}': artifactId,
'{version}': version.version.toString(),
'{branch}': branchLabel,
'{main_suffix}': isMain ? '_main' : '',
'{artifact_title}': sanitizeFileName(version.title),
'{command}': version.command,
'{extension}': getFileExtension(version.finalType, version.finalLanguage),
// Artifact content_start_timestamp variables
'{content_start_date}': contentStartDate.toISOString().split('T')[0],
'{content_start_time}': contentStartDate.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{content_start_year}': contentStartDate.getFullYear().toString(),
'{content_start_month}': String(contentStartDate.getMonth() + 1).padStart(2, '0'),
'{content_start_day}': String(contentStartDate.getDate()).padStart(2, '0'),
'{content_start_hour}': String(contentStartDate.getHours()).padStart(2, '0'),
'{content_start_minute}': String(contentStartDate.getMinutes()).padStart(2, '0'),
'{content_start_second}': String(contentStartDate.getSeconds()).padStart(2, '0'),
// Artifact content_stop_timestamp variables
'{content_stop_date}': contentStopDate.toISOString().split('T')[0],
'{content_stop_time}': contentStopDate.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{content_stop_year}': contentStopDate.getFullYear().toString(),
'{content_stop_month}': String(contentStopDate.getMonth() + 1).padStart(2, '0'),
'{content_stop_day}': String(contentStopDate.getDate()).padStart(2, '0'),
'{content_stop_hour}': String(contentStopDate.getHours()).padStart(2, '0'),
'{content_stop_minute}': String(contentStopDate.getMinutes()).padStart(2, '0'),
'{content_stop_second}': String(contentStopDate.getSeconds()).padStart(2, '0')
};
}
/**
* Formats timestamp according to the selected format
*/
function formatTimestamp(dateInput, format) {
const d = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
if (isNaN(d.getTime())) {
return formatTimestamp(new Date(), format);
}
const components = {
year: d.getFullYear(),
month: String(d.getMonth() + 1).padStart(2, '0'),
day: String(d.getDate()).padStart(2, '0'),
hour: String(d.getHours()).padStart(2, '0'),
minute: String(d.getMinutes()).padStart(2, '0'),
second: String(d.getSeconds()).padStart(2, '0')
};
switch (format) {
case 'YYYY-MM-DD_HH-MM-SS':
return `${components.year}-${components.month}-${components.day}_${components.hour}-${components.minute}-${components.second}`;
case 'ISO':
return `${components.year}-${components.month}-${components.day}T${components.hour}-${components.minute}-${components.second}`;
case 'YYYYMMDDHHMMSS':
default:
return `${components.year}${components.month}${components.day}${components.hour}${components.minute}${components.second}`;
}
}
/**
* Generates timestamp using current settings
*/
function generateTimestamp(dateInput) {
const settings = loadSettings();
const date = dateInput ?
(typeof dateInput === 'string' ? new Date(dateInput) : dateInput) :
new Date();
return formatTimestamp(date, settings.dateFormat);
}
/**
* Creates template variables object for filenames
*/
function createTemplateVariables(baseData) {
return { ...baseData };
}
/**
* Generates conversation filename using template
*/
function generateConversationFilename(conversationData) {
const settings = loadSettings();
const variables = {
...createStandardTemplateVariables(conversationData),
'{timestamp}': generateTimestamp(conversationData.updated_at)
};
return applyTemplate(settings.conversationTemplate, variables);
}
/**
* Generates artifact filename using template
*/
function generateArtifactFilename(version, conversationData, branchLabel, isMain, artifactId) {
const settings = loadSettings();
const variables = createArtifactTemplateVariables(version, conversationData, branchLabel, isMain, artifactId);
return applyTemplate(settings.artifactTemplate, variables);
}
/**
* Generates archive filename using template
*/
function generateArchiveName(conversationData, template, isMassExport = false, exportType = 'Conversation') {
const variables = {
...createStandardTemplateVariables(conversationData),
'{timestamp}': generateTimestamp(new Date()),
'{export_type}': exportType
};
return applyTemplate(template, variables) + '.zip';
}
/**
* Generates chat folder name using template
*/
function generateChatFolderName(conversationData, projectData, template) {
const variables = {
...createStandardTemplateVariables(conversationData),
'{timestamp}': generateTimestamp(new Date()),
'{project_name}': projectData ? sanitizeFileName(projectData.name) : '',
'{project_id}': projectData ? projectData.uuid : ''
};
return applyTemplate(template, variables);
}
// =============================================
// SETTINGS UI
// =============================================
/**
* Creates and shows the settings interface
*/
function showSettingsUI() {
// Remove existing settings UI if present
document.getElementById('claude-exporter-settings')?.remove();
const currentSettings = loadSettings();
// Create modal overlay
const overlay = document.createElement('div');
overlay.id = 'claude-exporter-settings';
overlay.innerHTML = createSettingsHTML(currentSettings);
// Add styles to head and modal to body
document.head.insertAdjacentHTML('beforeend', getSettingsStyles());
document.body.appendChild(overlay);
// Initialize functionality
initTabs();
initEventListeners();
updatePreviews();
}
/**
* Creates the settings HTML structure
*/
function createSettingsHTML(currentSettings) {
return `
🔧 Claude Exporter Settings
📦 Artifact Export Settings
When enabled, artifacts from messages that were stopped by user (stop_reason: "user_canceled") will be excluded from export. This helps avoid incomplete or unfinished artifacts.
When enabled, main branch export includes all conversation messages, but only main branch artifacts. When disabled, includes only main branch messages and artifacts.
This setting affects all export buttons: "Export Conversation + Final Artifacts", "Export Conversation + All Artifacts", and "Export Conversation + Latest Artifacts Per Message".
Choose which artifacts to include when using "Export Conversation Only" button.
📋 Export Behavior Summary
Conversation Only:
Final Artifacts:
All Artifacts:
Latest Per Message:
Main Branch Only:
🌿 Branch Export Settings
When enabled, only exports the conversation branch currently selected by user in Claude interface. Uses tree=false API parameter.
When enabled, main branch is determined from user-selected message (current_leaf_message_uuid) instead of message with highest index.
📝 Content Formatting
When enabled, the extracted content of attachments will not be included in the exported conversation markdown.
When enabled, replaces multiple consecutive newlines with single newlines in conversation text content only. Does not affect markdown structure or metadata.
When enabled, replaces multiple consecutive newlines with single newlines in markdown artifact content only. Does not affect artifact metadata or other file types.
When enabled, adds parent message UUID to message metadata. Useful for debugging conversation structure and understanding message relationships in complex branched conversations.
Chronological: API order. Logical: conversation tree with branches. Pairs: Human → Claude pairs in chronological order.
📝 Artifact File Settings
When enabled, artifact files will include metadata comments at the top (ID, branch, version, etc.). When disabled, files will contain only the pure artifact content.
These settings affect only single conversation exports, NOT mass exports.
When enabled, single conversation exports will be packaged into a ZIP archive.
Available variables: {timestamp}, {created_date}, {created_time}, {updated_date}, {updated_time}
When enabled, each conversation will be placed in its own folder within the archive.
Template for folder names. Default: {conversation_title}. Available variables: {timestamp}, {created_date}, {created_time}, {updated_date}, {updated_time}, {conversationId}, {conversation_title}
📁 Mass Export Archive Settings
These settings are available when Claude Mass Exporter is detected and only affect mass exports.
When enabled, mass exports will always use ZIP archives.
Available variables: {timestamp}, {export_type}, {created_date}, {created_time}, {updated_date}, {updated_time}
When enabled, mass exports will always create folders for each chat.
📁 Chat Folder Templates by Export Type
Template for exporting all projects. Example: {project_name}/{conversation_title}
Template for exporting from /project/xxx and /recents pages. Example: {conversation_title} or {created_date}/{conversation_title}
Available variables for all templates: {timestamp}, {created_date}, {created_time}, {updated_date}, {updated_time}, {conversationId}, {conversation_title}, {project_name}, {project_id}
📋 Export Menu Options
Control which export options appear in the Tampermonkey menu. All options are enabled by default.
When enabled, shows "Export Conversation Only" option in the menu.
When enabled, shows "Export Conversation + Final Artifacts" option in the menu.
When enabled, shows "Export Conversation + All Artifacts" option in the menu.
When enabled, shows "Export Conversation + Latest Artifacts Per Message" option in the menu.
When enabled, shows "Export Main Branch Only" option in the menu.
🔧 Raw API Export
When enabled, shows "Export Raw API Data" option in the menu. Exports conversation data as received from Claude API in JSON format.
`;
}
/**
* Returns compressed CSS styles for settings UI
*/
function getSettingsStyles() {
return ``;
}
function initTabs() {
document.querySelectorAll('.claude-tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.dataset.tab;
document.querySelectorAll('.claude-tab-btn, .claude-tab-content').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`tab-${targetTab}`).classList.add('active');
});
});
}
function initEventListeners() {
// Check for mass exporter and show/hide settings
const massExportSettings = document.getElementById('massExportArchiveSettings');
if (typeof window.claudeMassExporter !== 'undefined') {
massExportSettings.style.display = 'block';
}
setupConditionalSettings();
setupFormElementListeners();
setupVariablesPanel();
setupControlButtons();
setupModalHandlers();
}
function setupConditionalSettings() {
function updateConditionalElements() {
const exportToArchive = document.getElementById('exportToArchive').checked;
const createChatFolders = document.getElementById('createChatFolders').checked;
const forceArchive = document.getElementById('forceArchiveForMassExport').checked;
const forceChatFolders = document.getElementById('forceChatFoldersForMassExport').checked;
const enableRawApi = document.getElementById('enableRawApiExport').checked;
document.querySelectorAll('[data-depends="exportToArchive"]').forEach(el => {
el.style.display = exportToArchive ? 'block' : 'none';
});
document.querySelectorAll('[data-depends="createChatFolders,exportToArchive"]').forEach(el => {
el.style.display = (createChatFolders && exportToArchive) ? 'block' : 'none';
});
document.querySelectorAll('[data-depends="forceArchiveForMassExport"]').forEach(el => {
el.style.display = forceArchive ? 'block' : 'none';
});
document.querySelectorAll('[data-depends="forceChatFoldersForMassExport,forceArchiveForMassExport"]').forEach(el => {
el.style.display = (forceChatFolders && forceArchive) ? 'block' : 'none';
});
document.querySelectorAll('[data-depends="enableRawApiExport"]').forEach(el => {
el.style.display = enableRawApi ? 'block' : 'none';
});
}
['exportToArchive', 'createChatFolders', 'forceArchiveForMassExport', 'forceChatFoldersForMassExport', 'enableRawApiExport'].forEach(id => {
document.getElementById(id)?.addEventListener('change', updateConditionalElements);
});
updateConditionalElements();
}
function setupFormElementListeners() {
const formElements = ['conversationTemplate', 'artifactTemplate', 'dateFormat', 'artifactExportMode',
'excludeCanceledArtifacts', 'conversationOnlyArtifactMode', 'includeArtifactMetadata',
'excludeAttachments', 'removeDoubleNewlinesFromConversation', 'removeDoubleNewlinesFromMarkdown',
'includeParentMessageUuid', 'conversationSortMode', 'exportToArchive',
'createChatFolders', 'archiveName', 'chatFolderName',
'forceArchiveForMassExport', 'forceChatFoldersForMassExport', 'massExportArchiveName',
'massExportProjectsChatFolderName', 'massExportSingleChatFolderName',
'useCurrentLeafForMainBranch', 'exportOnlyCurrentBranch','mainBranchOnlyIncludeAllMessages',
'enableConversationOnly', 'enableFinalArtifacts', 'enableAllArtifacts',
'enableLatestPerMessage', 'enableMainBranchOnly', 'enableRawApiExport', 'rawApiExportFormat'];
formElements.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener(element.type === 'checkbox' ? 'change' : 'input', updatePreviews);
}
});
}
function setupVariablesPanel() {
document.querySelector('.claude-variables-toggle').addEventListener('click', (e) => {
e.preventDefault();
const panel = document.querySelector('.claude-variables-panel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
e.target.textContent = isVisible ? 'Show available variables' : 'Hide available variables';
});
}
function setupControlButtons() {
document.getElementById('resetDefaults').addEventListener('click', () => {
if (confirm('Reset all settings to defaults?')) {
Object.entries(defaultSettings).forEach(([key, value]) => {
const element = document.getElementById(key);
if (element) {
element.type === 'checkbox' ? element.checked = value : element.value = value;
}
});
updatePreviews();
}
});
document.getElementById('saveSettings').addEventListener('click', () => {
const newSettings = {};
Object.keys(defaultSettings).forEach(key => {
const element = document.getElementById(key);
if (element) {
newSettings[key] = element.type === 'checkbox' ? element.checked : element.value;
}
});
saveSettings(newSettings);
showNotification('Settings saved successfully!', 'success');
closeModal();
});
}
function setupModalHandlers() {
document.getElementById('cancelSettings').addEventListener('click', closeModal);
document.querySelector('.claude-settings-close').addEventListener('click', closeModal);
document.querySelector('.claude-settings-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('claude-settings-overlay')) closeModal();
});
}
function closeModal() {
document.getElementById('claude-exporter-settings')?.remove();
document.getElementById('claude-exporter-styles')?.remove();
}
function updatePreviews() {
const conversationTemplate = document.getElementById('conversationTemplate').value;
const artifactTemplate = document.getElementById('artifactTemplate').value;
const dateFormat = document.getElementById('dateFormat').value;
const artifactExportMode = document.getElementById('artifactExportMode').value;
const conversationOnlyArtifactMode = document.getElementById('conversationOnlyArtifactMode').value;
const mainBranchOnlyIncludeAllMessages = document.getElementById('mainBranchOnlyIncludeAllMessages').checked;
// Sample data for preview
const sampleTime = new Date();
const sampleVariables = createTemplateVariables({
'{timestamp}': formatTimestamp(sampleTime, dateFormat),
'{conversationId}': '12dasdh1-fa1j-f213-da13-dfa3124123ff',
'{conversation_title}': 'ConversationTitle',
'{artifact_title}': 'ArtifactTitle',
'{artifactId}': 'artifact2',
'{version}': '3',
'{branch}': '2',
'{main_suffix}': '_main',
'{command}': 'create',
'{extension}': '.js',
// Add all time variables
...createStandardTemplateVariables({
created_at: sampleTime.toISOString(),
updated_at: sampleTime.toISOString(),
uuid: '12dasdh1-fa1j-f213-da13-dfa3124123ff',
name: 'ConversationTitle'
})
});
document.getElementById('conversationPreview').textContent = 'Preview: ' + applyTemplate(conversationTemplate, sampleVariables);
document.getElementById('artifactPreview').textContent = 'Preview: ' + applyTemplate(artifactTemplate, sampleVariables);
// Update behavior descriptions
updateBehaviorDescriptions(artifactExportMode, conversationOnlyArtifactMode, mainBranchOnlyIncludeAllMessages);
}
function updateBehaviorDescriptions(artifactExportMode, conversationOnlyArtifactMode, mainBranchOnlyIncludeAllMessages) {
const behaviors = {
conversationOnlyBehavior: {
'none': 'Pure conversation without artifacts',
'final': 'Embeds final artifacts in conversation file',
'all': 'Embeds all artifacts in conversation file',
'latest_per_message': 'Embeds latest artifacts per message in conversation file',
'main_branch_only': mainBranchOnlyIncludeAllMessages ? 'All messages + main branch artifacts in conversation file' : 'Main branch messages + main branch artifacts in conversation file'
}[conversationOnlyArtifactMode],
finalArtifactsBehavior: {
'embed': 'Embeds final artifacts in conversation file only',
'files': 'Pure conversation + final artifacts as separate files',
'both': 'Embeds final artifacts in conversation + exports as separate files'
}[artifactExportMode],
allArtifactsBehavior: {
'embed': 'Embeds all artifacts in conversation file only',
'files': 'Pure conversation + all artifacts as separate files',
'both': 'Embeds all artifacts in conversation + exports as separate files'
}[artifactExportMode],
latestPerMessageBehavior: {
'embed': 'Embeds latest artifacts per message in conversation file only',
'files': 'Pure conversation + latest artifacts per message as separate files',
'both': 'Embeds latest artifacts per message in conversation + exports as separate files'
}[artifactExportMode],
mainBranchOnlyBehavior: (() => {
const messageMode = mainBranchOnlyIncludeAllMessages ? 'All messages' : 'Main branch messages';
return {
'embed': `${messageMode} + main branch artifacts in conversation file only`,
'files': `${messageMode} pure conversation + main branch artifacts as separate files`,
'both': `${messageMode} + main branch artifacts in conversation + exports as separate files`
}[artifactExportMode];
})()
};
Object.entries(behaviors).forEach(([id, text]) => {
const element = document.getElementById(id);
if (element) {
element.textContent = text;
}
});
}
// =============================================
// UTILITY FUNCTIONS
// =============================================
// BASIC UTILITIES
/**
* Sanitizes filename by removing invalid characters and limiting length
*/
function sanitizeFileName(name) {
return name.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
.replace(/__+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 100);
}
/**
* Formats date string to localized format
*/
function formatDate(dateInput) {
if (!dateInput) return '';
const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
return date.toLocaleString();
}
/**
* Downloads content as a file using browser's download functionality
*/
function downloadFile(filename, content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
}
// NOTIFICATION FUNCTIONS
/**
* Wraps async functions with error handling
*/
async function withErrorHandling(asyncFn, operationName) {
try {
return await asyncFn();
} catch (error) {
console.error(`[Claude API Exporter] ${operationName} failed:`, error);
showNotification(`${operationName} failed: ${error.message}`, 'error');
throw error;
}
}
/**
* Shows temporary notification to the user
* @param {string} message - Message to display
* @param {string} type - Type of notification (info, success, error)
*/
function showNotification(message, type = "info") {
// Remove any existing notifications to avoid overlap
document.querySelectorAll('.claude-notification').forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = 'claude-notification';
const colors = {
error: '#f44336',
success: '#4CAF50',
info: '#2196F3'
};
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 5px;
color: white;
font-family: system-ui, -apple-system, sans-serif;
font-size: 14px;
z-index: 10000;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
background-color: ${colors[type] || colors.info};
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
/**
* Generates appropriate notification message based on export results
*/
function generateExportNotification(exportResult, exportData) {
const { totalExported, artifactCount, archiveUsed } = exportResult;
const { settings, mode } = exportData;
if (archiveUsed) {
// Archive mode notifications
if (artifactCount > 0) {
return `Archive export completed! Downloaded archive with conversation + ${artifactCount} artifacts (${mode})`;
} else {
return `Archive export completed! Downloaded archive with conversation (no artifacts found)`;
}
} else {
// Regular download mode notifications
if (settings.artifactExportMode !== 'embed' && artifactCount > 0) {
// Has separate artifact files
let modeText;
if (settings.artifactExportMode === 'both') {
modeText = 'embedded + separate files';
} else if (settings.artifactExportMode === 'files') {
modeText = 'separate files';
} else {
modeText = 'embedded in conversation';
}
return `Export completed! Downloaded conversation + ${artifactCount} artifacts as ${modeText} (${mode})`;
} else if (artifactCount > 0) {
// Only embedded artifacts
return `Export completed! Downloaded conversation with ${artifactCount} embedded artifacts (${mode})`;
} else {
return 'Export completed! No artifacts found in conversation';
}
}
}
// =============================================
// ARCHIVE MANAGEMENT
// =============================================
/**
* Creates and manages ZIP archive for exports
*/
class ArchiveManager {
constructor() {
this.zip = null;
this.fileCount = 0;
this.isReady = false;
}
async initialize() {
if (this.isReady) return;
// console.log('[Claude API Exporter] Initializing ArchiveManager...');
await loadJSZip();
this.zip = new JSZip();
this.isReady = true;
// console.log('[Claude API Exporter] ArchiveManager initialized successfully');
}
async addFile(filename, content, useFolder = false, folderName = '') {
if (!this.isReady) {
await this.initialize();
}
let finalFilename = filename;
if (useFolder && folderName) {
finalFilename = `${sanitizeFileName(folderName)}/${filename}`;
}
// console.log(`[Claude API Exporter] Adding file to archive: ${finalFilename}`);
this.zip.file(finalFilename, content);
this.fileCount++;
// console.log(`[Claude API Exporter] File added. Total files: ${this.fileCount}`);
}
async downloadArchive(archiveName) {
if (!this.isReady) {
throw new Error('ArchiveManager not initialized');
}
if (this.fileCount === 0) {
throw new Error('No files to archive');
}
console.log(`[Claude API Exporter] Creating archive with ${this.fileCount} files...`);
showNotification(`Creating archive with ${this.fileCount} files...`, 'info');
try {
// console.log('[Claude API Exporter] Generating ZIP blob...');
const zipBlob = await this.zip.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: { level: 6 }
});
console.log('[Claude API Exporter] ZIP generated successfully, size:', zipBlob.size);
const url = URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
link.download = archiveName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// console.log('[Claude API Exporter] Archive download initiated');
// Cleanup
setTimeout(() => {
URL.revokeObjectURL(url);
// console.log('[Claude API Exporter] Archive cleanup completed');
}, 1000);
} catch (error) {
console.error('[Claude API Exporter] Archive generation failed:', error);
throw new Error(`Archive generation failed: ${error.message}`);
}
}
}
/**
* Enhanced download function that supports both regular and archive modes
*/
async function downloadFileEnhanced(filename, content, archiveManager = null, createFolder = false) {
if (archiveManager) {
await archiveManager.addFile(filename, content, createFolder);
} else {
downloadFile(filename, content);
}
}
/**
* Helper to add file to archive with folder support
*/
async function addFileToArchive(archiveManager, filename, content, conversationData, settings, projectData = null) {
if (!archiveManager) {
downloadFile(filename, content);
return;
}
const shouldUseFolder = settings.createChatFolders;
let folderName = '';
if (shouldUseFolder) {
folderName = generateChatFolderName(conversationData, projectData, settings.chatFolderName);
}
console.log(`[Claude API Exporter] Adding file to archive: ${filename}`);
await archiveManager.addFile(filename, content, shouldUseFolder, folderName);
}
// =============================================
// API FUNCTIONS
// =============================================
/**
* Extracts conversation ID from current URL
* @returns {string|null} Conversation ID or null if not found
*/
function getConversationId() {
const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
return match ? match[1] : null;
}
/**
* Gets organization ID from browser cookies
* @returns {string} Organization ID
* @throws {Error} If organization ID not found
*/
function getOrgId() {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'lastActiveOrg') {
return value;
}
}
throw new Error('Could not find organization ID');
}
/**
* Fetches conversation data from Claude API
* @param {string|null} conversationId - Conversation ID (if null, gets from URL)
* @returns {Promise