Claude API Exporter 1.4

Export Claude conversations using API

目前為 2025-07-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Claude API Exporter 1.4
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Export Claude conversations using API
// @author       MRL
// @match        https://claude.ai/*
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // =============================================
    // CONSTANTS AND DEFAULT SETTINGS
    // =============================================

    /**
     * Default settings configuration
     */
    const defaultSettings = {
        conversationTemplate: '{timestamp}_{conversationId}__{title}.md',
        artifactTemplate: '{timestamp}_{conversationId}_{artifactId}_branch{branch}{main_suffix}_v{version}_{title}{extension}',
        dateFormat: 'YYYYMMDDHHMMSS', // YYYYMMDDHHMMSS, YYYY-MM-DD_HH-MM-SS, ISO
        
        artifactExportMode: 'files', // Flexible artifact export settings: 'embed', 'files', 'both'
        includeArtifactsInConversationOnly: false, // For "Export Conversation Only" button
        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
    };

    /**
     * Available variables for filename templates
     */
    const availableVariables = {
        // Common variables
        '{timestamp}': 'Current timestamp (format depends on dateFormat setting)',
        '{conversationId}': 'Unique conversation identifier',
        '{title}': 'Sanitized conversation or artifact title',
        
        // 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',
        '{extension}': 'File extension based on artifact type',
        
        // Date/time variables
        '{date}': 'Current date in YYYY-MM-DD format',
        '{time}': 'Current time in HH-MM-SS format',
        '{year}': 'Current year (YYYY)',
        '{month}': 'Current month (MM)',
        '{day}': 'Current day (DD)',
        '{hour}': 'Current hour (HH)',
        '{minute}': 'Current minute (MM)',
        '{second}': 'Current 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 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 = `
            <div class="claude-settings-overlay">
                <div class="claude-settings-modal">
                    <div class="claude-settings-header">
                        <h2>🔧 Claude Exporter Settings</h2>
                        <button class="claude-settings-close" type="button">×</button>
                    </div>
                    
                    <!-- Tab Navigation -->
                    <div class="claude-tabs-nav">
                        <button class="claude-tab-btn active" data-tab="general">⚙️ General Settings</button>
                        <button class="claude-tab-btn" data-tab="filenames">📁 Filename Templates</button>
                    </div>
                    
                    <div class="claude-settings-content">
                        <!-- Tab 1: General Settings -->
                        <div class="claude-tab-content active" id="tab-general">
                            <div class="claude-settings-section">
                                <h3>📦 Artifact Export Settings</h3>
                                
                                <div class="claude-setting-group">
                                    <label for="artifactExportMode">Default artifact export mode:</label>
                                    <select id="artifactExportMode">
                                        ${['embed', 'files', 'both'].map(mode => 
                                            `<option value="${mode}" ${currentSettings.artifactExportMode === mode ? 'selected' : ''}>
                                                ${mode === 'embed' ? '📄 Embed Only - Include artifacts in conversation file only' : 
                                                  mode === 'files' ? '📁 Files Only - Export artifacts as separate files only' : 
                                                  '📄📁 Both - Include in conversation and export as separate files'}
                                            </option>`).join('')}
                                    </select>
                                    <p class="claude-settings-help">This setting affects the "Export Conversation + Final Artifacts" and "Export Conversation + All Artifacts" buttons.</p>
                                </div>

                                <div class="claude-setting-group">
                                    <div class="claude-checkbox-group">
                                        <input type="checkbox" id="includeArtifactsInConversationOnly" ${currentSettings.includeArtifactsInConversationOnly ? 'checked' : ''}>
                                        <label for="includeArtifactsInConversationOnly">Include all artifacts in "Export Conversation Only"</label>
                                    </div>
                                    <p class="claude-settings-help">When enabled, the "Export Conversation Only" button will embed all artifact versions in the conversation file (but never export separate artifact files).</p>
                                </div>

                                <div class="claude-export-behavior-info">
                                    <h4>📋 Export Behavior Summary</h4>
                                    <div class="claude-behavior-grid">
                                        ${['conversationOnlyBehavior', 'finalArtifactsBehavior', 'allArtifactsBehavior'].map(id => 
                                            `<div class="claude-behavior-item"><strong>${id.replace(/([A-Z])/g, ' $1').replace('Behavior', '').replace(/([a-z])([A-Z])/g, '$1 $2')}:</strong><span id="${id}"></span></div>`).join('')}
                                    </div>
                                </div>
                            </div>

                                <div class="claude-setting-group">
                                    <div class="claude-checkbox-group">
                                        <input type="checkbox" id="excludeCanceledArtifacts" ${currentSettings.excludeCanceledArtifacts ? 'checked' : ''}>
                                        <label for="excludeCanceledArtifacts">Skip artifacts from canceled messages</label>
                                    </div>
                                    <p class="claude-settings-help">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.</p>
                                </div>

                            <div class="claude-settings-section">
                                <h3>📝 Content Formatting</h3>
                                <div class="claude-setting-group">
                                    <div class="claude-checkbox-group">
                                        <input type="checkbox" id="excludeAttachments" ${currentSettings.excludeAttachments ? 'checked' : ''}>
                                        <label for="excludeAttachments">Exclude attachments content from conversation export</label>
                                    </div>
                                    <p class="claude-settings-help">When enabled, the extracted content of attachments will not be included in the exported conversation markdown.</p>
                                </div>
                                
                                ${['removeDoubleNewlinesFromConversation', 'removeDoubleNewlinesFromMarkdown'].map(setting => `
                                    <div class="claude-setting-group">
                                        <div class="claude-checkbox-group">
                                            <input type="checkbox" id="${setting}" ${currentSettings[setting] ? 'checked' : ''}>
                                            <label for="${setting}">Remove double newlines (\\n\\n) from ${setting.includes('Conversation') ? 'conversation content' : 'markdown artifact content'}</label>
                                        </div>
                                        <p class="claude-settings-help">When enabled, replaces multiple consecutive newlines with single newlines in ${setting.includes('Conversation') ? 'conversation text content only. Does not affect markdown structure or metadata.' : 'markdown artifact content only. Does not affect artifact metadata or other file types.'}</p>
                                    </div>
                                `).join('')}
                            </div>

                            <div class="claude-settings-section">
                                <h3>📝 Artifact File Settings</h3>
                                <div class="claude-setting-group">
                                    <div class="claude-checkbox-group">
                                        <input type="checkbox" id="includeArtifactMetadata" ${currentSettings.includeArtifactMetadata ? 'checked' : ''}>
                                        <label for="includeArtifactMetadata">Include metadata comments in artifact files</label>
                                    </div>
                                    <p class="claude-settings-help">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.</p>
                                </div>
                            </div>
                        </div>

                        <!-- Tab 2: Filename Templates -->
                        <div class="claude-tab-content" id="tab-filenames">
                            <div class="claude-settings-section">
                                <h3>📁 Filename Templates</h3>
                                <p class="claude-settings-description">Configure how exported files are named using variables. <a href="#claude-variables-panel" class="claude-variables-toggle">Show available variables</a></p>
                                
                                <div class="claude-variables-panel" style="display: none;">
                                    <h4>Available Variables:</h4>
                                    <div class="claude-variables-grid">
                                        ${Object.entries(availableVariables).map(([variable, description]) => 
                                            `<div class="claude-variable-item"><code class="claude-variable-name">${variable}</code><span class="claude-variable-desc">${description}</span></div>`).join('')}
                                    </div>
                                </div>

                                ${['conversation', 'artifact'].map(type => `
                                    <div class="claude-setting-group">
                                        <label for="${type}Template">${type.charAt(0).toUpperCase() + type.slice(1)} Files:</label>
                                        <input type="text" id="${type}Template" value="${currentSettings[type + 'Template']}" placeholder="${defaultSettings[type + 'Template']}">
                                        <div class="claude-preview" id="${type}Preview"></div>
                                    </div>
                                `).join('')}
                            </div>

                            <div class="claude-settings-section">
                                <h3>📅 Date Format</h3>
                                <div class="claude-setting-group">
                                    <label for="dateFormat">Timestamp Format:</label>
                                    <select id="dateFormat">
                                        ${[
                                            ['YYYYMMDDHHMMSS', 'Compact (YYYYMMDDHHMMSS) - e.g., 20250710143022'],
                                            ['YYYY-MM-DD_HH-MM-SS', 'Readable (YYYY-MM-DD_HH-MM-SS) - e.g., 2025-07-10_14-30-22'],
                                            ['ISO', 'ISO (YYYY-MM-DDTHH-MM-SS) - e.g., 2025-07-10T14-30-22']
                                        ].map(([value, text]) => 
                                            `<option value="${value}" ${currentSettings.dateFormat === value ? 'selected' : ''}>${text}</option>`).join('')}
                                    </select>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="claude-settings-footer">
                        <button class="claude-btn claude-btn-secondary" type="button" id="resetDefaults">🔄 Reset to Defaults</button>
                        <div class="claude-btn-group">
                            <button class="claude-btn claude-btn-secondary" type="button" id="cancelSettings">Cancel</button>
                            <button class="claude-btn claude-btn-primary" type="button" id="saveSettings">💾 Save Settings</button>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // Add CSS styles (compressed)
        const styles = `<style id="claude-exporter-styles">
            .claude-settings-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif}
            .claude-settings-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:800px;max-height:90vh;overflow:hidden;display:flex;flex-direction:column}
            .claude-settings-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between}
            .claude-settings-header h2{margin:0;font-size:20px;font-weight:600}
            .claude-settings-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-settings-close:hover{background:rgba(255,255,255,0.2)}
            .claude-tabs-nav{display:flex;background:#f8fafc;border-bottom:1px solid #e2e8f0}
            .claude-tab-btn{background:none;border:none;padding:16px 20px;font-size:14px;font-weight:500;color:#64748b;cursor:pointer;border-bottom:3px solid transparent;transition:all 0.2s;flex:1;text-align:center}
            .claude-tab-btn:hover{background:rgba(102,126,234,0.1);color:#475569}
            .claude-tab-btn.active{color:#667eea;background:white;border-bottom-color:#667eea}
            .claude-tab-content{display:none}.claude-tab-content.active{display:block}
            .claude-settings-content{flex:1;overflow-y:auto;padding:24px}
            .claude-settings-section{margin-bottom:32px}.claude-settings-section:last-child{margin-bottom:0}
            .claude-settings-section h3{margin:0 0 12px 0;font-size:18px;font-weight:600;color:#2d3748}
            .claude-settings-description{margin:0 0 20px 0;color:#718096;font-size:14px;line-height:1.5}
            .claude-variables-toggle{color:#667eea;text-decoration:none;font-weight:500}.claude-variables-toggle:hover{text-decoration:underline}
            .claude-variables-panel{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0}
            .claude-variables-panel h4{margin:0 0 12px 0;font-size:14px;font-weight:600;color:#2d3748}
            .claude-variables-grid{display:grid;gap:8px}
            .claude-variable-item{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start}
            .claude-variable-name{background:#e2e8f0;color:#2d3748;padding:2px 6px;border-radius:4px;font-family:'Monaco','Menlo',monospace;font-size:13px;white-space:nowrap}
            .claude-variable-desc{font-size:13px;color:#718096;line-height:1.4}
            .claude-setting-group{margin-bottom:20px}
            .claude-setting-group label{display:block;margin-bottom:6px;font-weight:500;color:#2d3748;font-size:14px}
            .claude-setting-group input,.claude-setting-group select{width:100%;padding:10px 12px;border:2px solid #e2e8f0;border-radius:6px;font-size:14px;transition:border-color 0.2s,box-shadow 0.2s;box-sizing:border-box}
            .claude-setting-group input[type="text"]{font-family:'Monaco','Menlo',monospace}
            .claude-checkbox-group{display:flex;align-items:center;gap:10px;margin-bottom:8px}
            .claude-checkbox-group input[type="checkbox"]{width:auto;margin:0;transform:scale(1.2)}
            .claude-checkbox-group label{margin:0;cursor:pointer;font-weight:500}
            .claude-settings-help{margin:8px 0 0 0;font-size:13px;color:#718096;line-height:1.4}
            .claude-setting-group input:focus,.claude-setting-group select:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.1)}
            .claude-preview{margin-top:8px;padding:8px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:6px;font-family:'Monaco','Menlo',monospace;font-size:13px;color:#0369a1;word-break:break-all}
            .claude-export-behavior-info{background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:16px;margin-top:20px}
            .claude-export-behavior-info h4{margin:0 0 12px 0;font-size:14px;font-weight:600;color:#0369a1}
            .claude-behavior-grid{display:grid;gap:8px}
            .claude-behavior-item{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start}
            .claude-behavior-item strong{font-size:13px;color:#0369a1;white-space:nowrap}
            .claude-behavior-item span{font-size:13px;color:#374151;line-height:1.4}
            .claude-settings-footer{background:#f8fafc;padding:20px 24px;border-top:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between}
            .claude-btn{padding:10px 20px;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:all 0.2s;text-decoration:none;display:inline-flex;align-items:center;gap:6px}
            .claude-btn-primary{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white}
            .claude-btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,0.4)}
            .claude-btn-secondary{background:#e2e8f0;color:#2d3748}.claude-btn-secondary:hover{background:#cbd5e0}
            .claude-btn-group{display:flex;gap:12px}
            @media (max-width:600px){.claude-settings-modal{width:95%;margin:20px}.claude-settings-footer{flex-direction:column;gap:12px}.claude-btn-group{width:100%}.claude-btn{flex:1;justify-content:center}.claude-tabs-nav{flex-direction:column}.claude-tab-btn{flex:none}}
        </style>`;

        // Add styles to head and modal to body
        document.head.insertAdjacentHTML('beforeend', styles);
        document.body.appendChild(overlay);

        // Initialize functionality
        initTabs();
        initEventListeners();
        updatePreviews();
    }

        // Tab switching functionality
        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');
                });
            });
        }
    
        // Initialize all event listeners
        function initEventListeners() {
            const formElements = ['conversationTemplate', 'artifactTemplate', 'dateFormat', 'artifactExportMode', 
                                'excludeCanceledArtifacts', 'includeArtifactsInConversationOnly', 'includeArtifactMetadata', 
                                'excludeAttachments', 'removeDoubleNewlinesFromConversation', 'removeDoubleNewlinesFromMarkdown'];
            
            formElements.forEach(id => {
                const element = document.getElementById(id);
                if (element) {
                    element.addEventListener(element.type === 'checkbox' ? 'change' : 'input', updatePreviews);
                }
            });
    
            // Variables panel toggle
            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';
            });
    
            // Reset defaults
            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();
                }
            });
    
            // Close modal functionality
            const closeModal = () => {
                document.getElementById('claude-exporter-settings')?.remove();
                document.getElementById('claude-exporter-styles')?.remove();
            };
    
            document.getElementById('cancelSettings').addEventListener('click', closeModal);
            document.querySelector('.claude-settings-close').addEventListener('click', closeModal);
            
            // Close on overlay click
            document.querySelector('.claude-settings-overlay').addEventListener('click', (e) => {
                if (e.target.classList.contains('claude-settings-overlay')) closeModal();
            });
    
            // Save settings
            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();
            });
        }
    
        // Update previews function
        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 includeArtifactsInConversationOnly = document.getElementById('includeArtifactsInConversationOnly').checked;
    
            // Sample data for preview
            const sampleVariables = createTemplateVariables({
                '{timestamp}': formatTimestamp(new Date(), dateFormat),
                '{conversationId}': '12dasdh1-fa1j-f213-da13-dfa3124123ff',
                '{title}': 'Title',
                '{artifactId}': 'artifact2',
                '{version}': '3',
                '{branch}': '2',
                '{main_suffix}': '',
                '{extension}': '.js'
            });
    
            document.getElementById('conversationPreview').textContent = 'Preview: ' + applyTemplate(conversationTemplate, sampleVariables);
            document.getElementById('artifactPreview').textContent = 'Preview: ' + applyTemplate(artifactTemplate, sampleVariables);
            
            // Behavior descriptions
            const behaviors = {
                conversationOnlyBehavior: includeArtifactsInConversationOnly ? 'Embeds all artifacts in conversation file' : 'Pure conversation without artifacts',
                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]
            };
    
            Object.entries(behaviors).forEach(([id, text]) => {
                document.getElementById(id).textContent = text;
            });
        }

    // =============================================
    // SETTINGS UTILITY FUNCTIONS
    // =============================================

    /**
     * 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) {
        const now = new Date();
        const base = {
            '{date}': now.toISOString().split('T')[0],
            '{time}': now.toTimeString().split(' ')[0].replace(/:/g, '-'),
            '{year}': now.getFullYear().toString(),
            '{month}': String(now.getMonth() + 1).padStart(2, '0'),
            '{day}': String(now.getDate()).padStart(2, '0'),
            '{hour}': String(now.getHours()).padStart(2, '0'),
            '{minute}': String(now.getMinutes()).padStart(2, '0'),
            '{second}': String(now.getSeconds()).padStart(2, '0')
        };
        
        return { ...base, ...baseData };
    }

    /**
     * Generates conversation filename using template
     */
    function generateConversationFilename(conversationData) {
        const settings = loadSettings();
        const variables = createTemplateVariables({
            '{timestamp}': generateTimestamp(conversationData.updated_at),
            '{conversationId}': conversationData.uuid,
            '{title}': sanitizeFileName(conversationData.name)
        });
        
        return applyTemplate(settings.conversationTemplate, variables);
    }

    /**
     * Generates artifact filename using template
     */
    function generateArtifactFilename(version, conversationData, branchLabel, isMain, artifactId) {
        const settings = loadSettings();
        const variables = createTemplateVariables({
            '{timestamp}': generateTimestamp(version.content_stop_timestamp),
            '{conversationId}': conversationData.uuid,
            '{artifactId}': artifactId,
            '{version}': version.version.toString(),
            '{branch}': branchLabel,
            '{main_suffix}': isMain ? '_main' : '',
            '{title}': sanitizeFileName(version.title),
            '{extension}': getFileExtension(version.finalType, version.finalLanguage)
        });
        
        return applyTemplate(settings.artifactTemplate, variables);
    }
    
    /**
     * Removes double newlines from text content while preserving markdown structure
     * @param {string} content - Text content to process
     * @param {boolean} removeDoubleNewlines - Whether to remove \n\n
     * @returns {string} Processed content
     */
    function processTextContent(content, removeDoubleNewlines) {
        return processContent(content, removeDoubleNewlines);
    }

    /**
     * Processes artifact content based on type and settings
     * @param {string} content - Artifact content
     * @param {string} type - Artifact type
     * @param {boolean} removeDoubleNewlines - Whether to remove \n\n
     * @returns {string} Processed content
     */
    function processArtifactContent(content, type, removeDoubleNewlines) {
        return processContent(content, removeDoubleNewlines, type);
    }

    /**
     * Unified content processing function
     */
    function processContent(content, removeDoubleNewlines, type = null) {
        if (!removeDoubleNewlines || !content) {
            return content;
        }
        
        // Only apply to markdown artifacts or general content
        if (!type || type === 'text/markdown') {
            return content.replace(/\n\n+/g, '\n');
        }
        
        return content;
    }

    // =============================================
    // UTILITY FUNCTIONS
    // =============================================

    /**
     * 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);
    }

    /**
     * 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") {
        const notification = document.createElement('div');
        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);
    }

    // =============================================
    // 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
     * @returns {Promise<Object>} Complete conversation data including messages and metadata
     */
    async function getConversationData() {
        const conversationId = getConversationId();
        if (!conversationId) {
            throw new Error('Not in a conversation');
        }

        const orgId = getOrgId();
        const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true`);

        if (!response.ok) {
            throw new Error(`API request failed: ${response.status}`);
        }

        return await response.json();
    }

    // =============================================
    // FILE EXTENSION FUNCTIONS
    // =============================================

    /**
     * Gets appropriate file extension based on artifact type and language
     * @param {string} type - Artifact MIME type
     * @param {string} language - Programming language (for code artifacts)
     * @returns {string} File extension including the dot
     */
    function getFileExtension(type, language) {
        switch (type) {
            case 'application/vnd.ant.code':
                return getCodeExtension(language);
            case 'text/html':
                return '.html';
            case 'text/markdown':
                return '.md';
            case 'image/svg+xml':
                return '.svg';
            case 'application/vnd.ant.mermaid':
                return '.mmd';
            case 'application/vnd.ant.react':
                return '.jsx';
            case undefined:
            default:
                return '.txt';
        }
    }

    /**
     * Maps programming language names to file extensions
     * @param {string} language - Programming language name
     * @returns {string} File extension including the dot
     */
    function getCodeExtension(language) {
        const extensionMap = {
            // Web languages
            'javascript': '.js', 'typescript': '.ts', 'html': '.html', 'css': '.css', 'scss': '.scss', 'sass': '.sass', 'less': '.less', 'jsx': '.jsx', 'tsx': '.tsx', 'vue': '.vue',

            // Languages
            'python': '.py', 'java': '.java', 'csharp': '.cs', 'c#': '.cs', 'cpp': '.cpp', 'c++': '.cpp', 'c': '.c', 'go': '.go', 'rust': '.rs', 'swift': '.swift', 'kotlin': '.kt', 'dart': '.dart', 'php': '.php', 'ruby': '.rb', 'perl': '.pl', 'lua': '.lua',

            // Functional languages
            'haskell': '.hs', 'clojure': '.clj', 'erlang': '.erl', 'elixir': '.ex', 'fsharp': '.fs', 'f#': '.fs', 'ocaml': '.ml', 'scala': '.scala', 'lisp': '.lisp',

            // Data and config
            'json': '.json', 'yaml': '.yaml', 'yml': '.yml', 'xml': '.xml', 'toml': '.toml', 'ini': '.ini', 'csv': '.csv',

            // Query languages
            'sql': '.sql', 'mysql': '.sql', 'postgresql': '.sql', 'sqlite': '.sql', 'plsql': '.sql',

            // Shell and scripting
            'bash': '.sh', 'shell': '.sh', 'sh': '.sh', 'zsh': '.zsh', 'fish': '.fish', 'powershell': '.ps1', 'batch': '.bat', 'cmd': '.cmd',

            // Scientific and specialized
            'r': '.r', 'matlab': '.m', 'julia': '.jl', 'fortran': '.f90', 'cobol': '.cob', 'assembly': '.asm', 'vhdl': '.vhd', 'verilog': '.v',

            // Build and config files
            'dockerfile': '.dockerfile', 'makefile': '.mk', 'cmake': '.cmake', 'gradle': '.gradle', 'maven': '.xml',

            // Markup and documentation
            'markdown': '.md', 'latex': '.tex', 'restructuredtext': '.rst', 'asciidoc': '.adoc',

            // Other
            'regex': '.regex', 'text': '.txt', 'plain': '.txt'
        };

        const normalizedLanguage = language ? language.toLowerCase().trim() : '';
        return extensionMap[normalizedLanguage] || '.txt';
    }

    /**
     * Gets comment style for a programming language
     * @param {string} type - Artifact MIME type
     * @param {string} language - Programming language
     * @returns {Object} Comment style object with start and end strings
     */
    function getCommentStyle(type, language) {
        if (type === 'text/html' || type === 'image/svg+xml') {
            return { start: '<!-- ', end: ' -->' };
        }

        if (type !== 'application/vnd.ant.code') {
            return { start: '# ', end: '' }; // Default to hash comments
        }

        const normalizedLanguage = language ? language.toLowerCase().trim() : '';
        
        // Define comment patterns
        const commentPatterns = {
            // Languages with // comments
            slash: ['javascript', 'typescript', 'java', 'csharp', 'c#', 'cpp', 'c++', 'c', 'go', 'rust', 'swift', 'kotlin', 'dart', 'php', 'scala', 'jsx', 'tsx'],
                   
            // Languages with # comments
            hash: ['python', 'ruby', 'perl', 'bash', 'shell', 'sh', 'zsh', 'fish', 'yaml', 'yml', 'r', 'julia', 'toml', 'ini', 'powershell'],
                  
            // Languages with -- comments
            dash: ['sql', 'mysql', 'postgresql', 'sqlite', 'plsql', 'haskell', 'lua']
        };

        if (commentPatterns.slash.includes(normalizedLanguage)) {
            return { start: '// ', end: '' };
        } else if (commentPatterns.hash.includes(normalizedLanguage)) {
            return { start: '# ', end: '' };
        } else if (commentPatterns.dash.includes(normalizedLanguage)) {
            return { start: '-- ', end: '' };
        }
        
        return { start: '# ', end: '' }; // Default to hash comments
    }

    /**
     * Gets language identifier for markdown syntax highlighting
     */
    function getLanguageForHighlighting(type, language) {
        const typeMap = {
            'text/html': 'html',
            'text/markdown': 'markdown', 
            'image/svg+xml': 'xml',
            'application/vnd.ant.mermaid': 'mermaid',
            'application/vnd.ant.react': 'jsx'
        };
        
        if (typeMap[type]) return typeMap[type];
        
        if (type === 'application/vnd.ant.code' && language) {
            const normalizedLanguage = language.toLowerCase().trim();
            
            // Map some languages to their markdown equivalents
            const languageMap = {
                'c++': 'cpp', 'c#': 'csharp', 'f#': 'fsharp',
                'objective-c': 'objc', 'shell': 'bash', 'sh': 'bash'
            };
            
            return languageMap[normalizedLanguage] || normalizedLanguage;
        }
        
        return '';
    }

    // =============================================
    // BRANCH HANDLING FUNCTIONS
    // =============================================

    /**
     * Builds conversation tree structure to understand message branches
     * @param {Array} messages - Array of chat messages
     * @returns {Object} Tree structure with branch information
     */
    function buildConversationTree(messages) {
        const messageMap = new Map();
        const rootMessages = [];
        
        // First pass: create message map
        messages.forEach(message => {
            messageMap.set(message.uuid, {
                ...message,
                children: [],
                branchId: null,
                branchIndex: null
            });
        });
        
        // Second pass: build parent-child relationships
        messages.forEach(message => {
            const messageNode = messageMap.get(message.uuid);
            if (message.parent_message_uuid && messageMap.has(message.parent_message_uuid)) {
                const parent = messageMap.get(message.parent_message_uuid);
                parent.children.push(messageNode);
            } else {
                rootMessages.push(messageNode);
            }
        });
        
        return {
            messageMap,
            rootMessages
        };
    }

    /**
     * Gets all branch information including branch points
     * @param {Object} tree - Tree structure from buildConversationTree
     * @returns {Array} Array of branch information
     */
    function getAllBranchInfo(tree) {
        const branches = [];
        let branchCounter = 0;
        
        function traverseBranches(node, currentPath = [], branchStartIndex = 0) {
            const newPath = [...currentPath, node];
            
            if (node.children.length === 0) {
                // Leaf node - this is a complete branch
                branchCounter++;
                branches.push({
                    branchId: node.uuid,
                    branchIndex: branchCounter,
                    fullPath: newPath,
                    branchStartIndex: branchStartIndex, // Index in fullPath where this branch starts
                    isMainBranch: branchStartIndex === 0
                });
            } else if (node.children.length === 1) {
                // Single child - continue same branch
                traverseBranches(node.children[0], newPath, branchStartIndex);
            } else {
                // Multiple children - branch point
                node.children.forEach((child, childIndex) => {
                    // For first child, continue current branch
                    // For other children, start new branches from this point
                    const newBranchStart = childIndex === 0 ? branchStartIndex : newPath.length;
                    traverseBranches(child, newPath, newBranchStart);
                });
            }
        }
        
        tree.rootMessages.forEach(root => {
            traverseBranches(root, [], 0);
        });
        
        return branches;
    }

    // =============================================
    // TEXT PROCESSING FUNCTIONS
    // =============================================

    /**
     * Recursively extracts text content from nested content structures
     * @param {Object} content - Content object to process
     * @returns {Promise<Array<string>>} Array of text pieces found
     */
    async function getTextFromContent(content) {
        let textPieces = [];

        if (content.text) {
            textPieces.push(content.text);
        }
        if (content.input) {
            textPieces.push(JSON.stringify(content.input));
        }
        if (content.content) {
            if (Array.isArray(content.content)) {
                for (const nestedContent of content.content) {
                    textPieces = textPieces.concat(await getTextFromContent(nestedContent));
                }
            } else if (typeof content.content === 'object') {
                textPieces = textPieces.concat(await getTextFromContent(content.content));
            }
        }
        return textPieces;
    }

    // =============================================
    // ARTIFACT PROCESSING FUNCTIONS
    // =============================================

    /**
     * Extracts artifacts from messages, respecting branch boundaries
     * @param {Array} branchPath - Full path from root to leaf
     * @param {number} branchStartIndex - Index where this branch starts (for split branches)
     * @param {string} branchId - Unique identifier for this branch
     * @param {boolean} isMainBranch - Whether this is the main branch
     * @returns {Object} {ownArtifacts: Map, inheritedStates: Map}
     */
    function extractArtifacts(branchPath, branchStartIndex, branchId, isMainBranch) {
        const settings = loadSettings();
        
        const ownArtifacts = new Map(); // Artifacts created/modified in this branch
        const inheritedStates = new Map(); // Final states of artifacts from parent branch
        
        // For non-main branches, first collect inherited states from parent path
        if (!isMainBranch && branchStartIndex > 0) {
            const parentPath = branchPath.slice(0, branchStartIndex);
            const parentArtifacts = new Map();
            
            // Extract artifacts from parent path
            parentPath.forEach((message, messageIndex) => {
                message.content.forEach(content => {
                    if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                        const input = content.input;
                        const artifactId = input.id;
                        
                        if (!parentArtifacts.has(artifactId)) {
                            parentArtifacts.set(artifactId, []);
                        }
                        
                        const versions = parentArtifacts.get(artifactId);
                        versions.push({
                            type: input.type,
                            title: input.title || `Artifact ${artifactId}`,
                            command: input.command,
                            content: input.content || '',
                            new_str: input.new_str || '',
                            old_str: input.old_str || '',
                            language: input.language || '',
                            timestamp_created_at: message.created_at,
                            timestamp_updated_at: message.updated_at
                        });
                    }
                });
            });
            
            // Build final states from parent artifacts
            parentArtifacts.forEach((versions, artifactId) => {
                let currentContent = '';
                let currentTitle = `Artifact ${artifactId}`;
                let currentType = undefined;
                let currentLanguage = '';
                let versionCount = 0;
                
                versions.forEach(version => {
                    versionCount++;
                    switch (version.command) {
                        case 'create':
                            currentContent = version.content;
                            currentTitle = version.title;
                            currentType = version.type;
                            currentLanguage = version.language;
                            break;
                        case 'rewrite':
                            currentContent = version.content;
                            currentTitle = version.title;
                            // Keep type and language from create
                            break;
                        case 'update':
                            const updateResult = applyUpdate(currentContent, version.old_str, version.new_str);
                            currentContent = updateResult.content;
                            break;
                    }
                });
                
                inheritedStates.set(artifactId, {
                    content: currentContent,
                    title: currentTitle,
                    type: currentType,
                    language: currentLanguage,
                    versionCount: versionCount
                });
            });
        }
        
        // Now extract artifacts from this branch only (starting from branchStartIndex)
        const branchMessages = branchPath.slice(branchStartIndex);
        
        branchMessages.forEach((message, relativeIndex) => {
            // User canceled message - excludeCanceledArtifacts
            // if (settings.excludeCanceledArtifacts && message.stop_reason === 'user_canceled') {
                // return; // skip this message
            // }
            
            message.content.forEach(content => {
                if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                    const input = content.input;
                    const artifactId = input.id;

                    if (!ownArtifacts.has(artifactId)) {
                        ownArtifacts.set(artifactId, []);
                    }

                    const versions = ownArtifacts.get(artifactId);
                    
                    // Calculate version number based on inherited versions
                    let versionNumber;
                    if (isMainBranch) {
                        // Main branch: start from 1
                        versionNumber = versions.length + 1;
                    } else {
                        // Split branch: continue from parent version count
                        const inheritedCount = inheritedStates.has(artifactId) ? inheritedStates.get(artifactId).versionCount : 0;
                        versionNumber = inheritedCount + versions.length + 1;
                    }
                    
                    versions.push({
                        messageUuid: message.uuid,
                        messageText: message.text,
                        version: versionNumber,
                        content_start_timestamp: content.start_timestamp,
                        content_stop_timestamp: content.stop_timestamp,
                        content_type: content.type,
                        type: input.type,
                        title: input.title || `Artifact ${artifactId}`,
                        command: input.command,
                        old_str: input.old_str || '',
                        new_str: input.new_str || '',
                        content: input.content || '',
                        uuid: input.version_uuid,
                        language: input.language || '',
                        messageIndex: branchStartIndex + relativeIndex,
                        stop_reason: message.stop_reason,
                        timestamp_created_at: message.created_at,
                        timestamp_updated_at: message.updated_at,
                        branchId: branchId
                    });
                }
            });
        });

        return { ownArtifacts, inheritedStates };
    }

    /**
     * Applies update command to previous content by replacing old_str with new_str
     * @param {string} previousContent - Content before update
     * @param {string} oldStr - String to be replaced
     * @param {string} newStr - String to replace with
     * @returns {Object} {success: boolean, content: string, info: string}
     */
    function applyUpdate(previousContent, oldStr, newStr) {
        if (!previousContent || !oldStr) {
            // If no old_str or previousContent, prepend new_str to beginning
            if (newStr) {
                return {
                    success: true,
                    content: newStr + (previousContent ? '\n' + previousContent : ''),
                    info: '[WARNING: Added content to beginning - missing old_str or previousContent]'
                };
            }
            return {
                success: false,
                content: previousContent || '',
                info: 'Cannot apply update: missing previousContent, oldStr, and newStr'
            };
        }

        // Apply the string replacement
        const updatedContent = previousContent.replace(oldStr, newStr);

        if (updatedContent === previousContent) {
            // old_str not found - prepend new_str to beginning as fallback
            if (newStr) {
                return {
                    success: true,
                    content: newStr + '\n' + previousContent,
                    info: '[WARNING: Added content to beginning - old_str not found in content]'
                };
            }
            
            // Try to find similar strings for debugging
            const lines = previousContent.split('\n');
            const oldLines = oldStr.split('\n');
            let debugInfo = 'Update did not change content - old string not found';
            
            if (oldLines.length > 0) {
                const firstOldLine = oldLines[0].trim();
                const foundLine = lines.find(line => line.includes(firstOldLine));
                if (foundLine) {
                    debugInfo += ` | Found similar line: "${foundLine.trim()}"`;
                }
            }
            
            return {
                success: false,
                content: previousContent,
                info: debugInfo
            };
        }

        return {
            success: true,
            content: updatedContent,
            info: `Successfully applied update`
        };
    }

    /**
     * Creates change description for artifact commands
     */
    function createChangeDescription(version, oldContent, currentContent) {
        switch (version.command) {
            case 'create':
                return 'Created';
            case 'rewrite':
                return 'Rewritten';
            case 'update':
                const oldPreview = version.old_str ? version.old_str.substring(0, 50).replace(/\n/g, '\\n') + '...' : '';
                const newPreview = version.new_str ? version.new_str.substring(0, 50).replace(/\n/g, '\\n') + '...' : '';
                let changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`;
                
                // Add information about character count changes
                const lengthDiff = currentContent.length - oldContent.length;
                if (lengthDiff > 0) {
                    changeDescription += ` (+${lengthDiff} chars)`;
                } else if (lengthDiff < 0) {
                    changeDescription += ` (${lengthDiff} chars)`;
                }
                
                return changeDescription;
            default:
                return `Unknown command: ${version.command}`;
        }
    }

    /**
     * Builds complete artifact versions for a specific branch
     * @param {Map} ownArtifacts - Artifacts created/modified in this branch
     * @param {Map} inheritedStates - Final states from parent branch
     * @param {string} branchId - Branch identifier
     * @param {boolean} isMainBranch - Whether this is the main branch
     * @returns {Map} Map of artifact ID to processed versions with full content
     */
    function buildArtifactVersions(ownArtifacts, inheritedStates, branchId, isMainBranch) {
        const processedArtifacts = new Map();

        ownArtifacts.forEach((versions, artifactId) => {
            const processedVersions = [];
            
            // Start with inherited content if this is a branch
            let currentContent = '';
            let currentTitle = `Artifact ${artifactId}`;
            let currentType = undefined;
            let currentLanguage = '';
            
            if (!isMainBranch && inheritedStates.has(artifactId)) {
                const inherited = inheritedStates.get(artifactId);
                currentContent = inherited.content;
                currentTitle = inherited.title;
                currentType = inherited.type;
                currentLanguage = inherited.language;
            }

            versions.forEach((version, index) => {
                let updateInfo = '';
                let versionStartContent = currentContent;
                const oldContent = currentContent;
                
                switch (version.command) {
                    case 'create':
                        currentContent = version.content;
                        currentTitle = version.title;
                        currentType = version.type;
                        currentLanguage = version.language;
                        break;
                    case 'rewrite':
                        currentContent = version.content;
                        currentTitle = version.title;
                        // Keep type and language from create
                        break;
                    case 'update':
                        const updateResult = applyUpdate(currentContent, version.old_str, version.new_str);
                        
                        currentContent = updateResult.content;
                        updateInfo = updateResult.info;
                        
                        if (!updateResult.success) {
                            updateInfo = `[WARNING: ${updateResult.info}]`;
                        }
                        break;
                    default:
                        console.warn(`Unknown command: ${version.command}`);
                        break;
                }

                const changeDescription = createChangeDescription(version, oldContent, currentContent);

                processedVersions.push({
                    ...version,
                    fullContent: currentContent,
                    changeDescription: updateInfo ? changeDescription + ` ${updateInfo}` : changeDescription,
                    updateInfo: updateInfo,
                    branchId: branchId,
                    isMainBranch: isMainBranch,
                    inheritedContent: versionStartContent,
                    finalType: currentType,
                    finalLanguage: currentLanguage
                });
            });

            processedArtifacts.set(artifactId, processedVersions);
        });

        return processedArtifacts;
    }

    /**
     * Extracts and processes all artifacts from all branches with proper inheritance
     * @param {Object} conversationData - Complete conversation data
     * @returns {Object} {branchArtifacts: Map, branchInfo: Array}
     */
    function extractAllArtifacts(conversationData) {
        // Build conversation tree
        const tree = buildConversationTree(conversationData.chat_messages);
        const branches = getAllBranchInfo(tree);
        
        console.log(`Found ${branches.length} conversation branches`);
        
        const branchArtifacts = new Map(); // branchId -> Map<artifactId, versions>
        const branchInfo = [];
        
        branches.forEach((branch) => {
            const { ownArtifacts, inheritedStates } = extractArtifacts(
                branch.fullPath, 
                branch.branchStartIndex, 
                branch.branchId, 
                branch.isMainBranch
            );
            
            if (ownArtifacts.size > 0) {
                // Process artifacts for this branch
                const processedArtifacts = buildArtifactVersions(
                    ownArtifacts, 
                    inheritedStates, 
                    branch.branchId, 
                    branch.isMainBranch
                );
                branchArtifacts.set(branch.branchId, processedArtifacts);
                
                const leafMessage = branch.fullPath[branch.fullPath.length - 1];
                branchInfo.push({
                    branchId: branch.branchId,
                    branchIndex: branch.branchIndex,
                    messageCount: branch.fullPath.length,
                    branchMessageCount: branch.fullPath.length - branch.branchStartIndex,
                    artifactCount: ownArtifacts.size,
                    inheritedArtifactCount: inheritedStates.size,
                    lastMessageTime: leafMessage.created_at,
                    lastMessageUuid: leafMessage.uuid,
                    isMainBranch: branch.isMainBranch,
                    branchStartIndex: branch.branchStartIndex
                });
            }
        });
        
        return {
            branchArtifacts,
            branchInfo
        };
    }

    // =============================================
    // VERSION TRACKING FUNCTIONS
    // =============================================

    /**
     * Builds version information for messages with alternatives (same parent)
     * @param {Array} messages - Array of chat messages
     * @returns {Map} Map of message UUID to version info {version, total}
     */
    function buildVersionInfo(messages) {
        const versionInfo = new Map();
        
        // Group messages by parent_message_uuid
        const parentGroups = new Map();
        
        messages.forEach(message => {
            if (message.parent_message_uuid) {
                if (!parentGroups.has(message.parent_message_uuid)) {
                    parentGroups.set(message.parent_message_uuid, []);
                }
                parentGroups.get(message.parent_message_uuid).push(message);
            }
        });
        
        // Process groups with more than one message (alternatives)
        parentGroups.forEach((siblings, parentUuid) => {
            if (siblings.length > 1) {
                // Sort by created_at to determine version numbers
                siblings.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
                
                siblings.forEach((message, index) => {
                    versionInfo.set(message.uuid, {
                        version: index + 1,
                        total: siblings.length
                    });
                });
            }
        });
        
        return versionInfo;
    }

    // =============================================
    // EXPORT FUNCTIONS
    // =============================================

    /**
     * Finds the artifact content for a specific artifact ID and message
     */
    function findArtifactContent(artifactId, messageUuid, branchArtifacts, includeMode = 'final', stopTimestamp = null) {
        let allVersionsOfArtifact = [];
        let messageVersion = null;
        
        // Collect all versions of this artifact from all branches
        for (const [branchId, artifactsMap] of branchArtifacts) {
            if (artifactsMap.has(artifactId)) {
                const versions = artifactsMap.get(artifactId);
                allVersionsOfArtifact = allVersionsOfArtifact.concat(versions);
                
                // Find the specific version by timestamp if provided
                if (stopTimestamp) {
                    const specificVersion = versions.find(v => 
                        v.messageUuid === messageUuid && 
                        v.content_stop_timestamp === stopTimestamp
                    );
                    if (specificVersion) {
                        messageVersion = specificVersion;
                    }
                } else {
                    // Fallback: find any version in this message
                    const msgVersion = versions.find(v => v.messageUuid === messageUuid);
                    if (msgVersion) {
                        messageVersion = msgVersion;
                    }
                }
            }
        }
        
        if (allVersionsOfArtifact.length === 0) {
            return null;
        }
        
        if (includeMode === 'all') {
            // Show the specific version that was created in this tool_use
            return messageVersion;
        } else if (includeMode === 'final') {
            // Sort all versions by creation time to find the truly latest one
            allVersionsOfArtifact.sort((a, b) => {
                const timeA = new Date(a.content_stop_timestamp || a.timestamp_created_at);
                const timeB = new Date(b.content_stop_timestamp || b.timestamp_created_at);
                return timeA - timeB;
            });
            
            const globalLatestVersion = allVersionsOfArtifact[allVersionsOfArtifact.length - 1];
            
            // Show artifact ONLY if this message contains the globally final version
            if (globalLatestVersion.messageUuid === messageUuid) {
                return globalLatestVersion;
            }
            
            return null;
        }
        
        return null;
    }

    /**
     * Generates markdown content for the entire conversation
     * @param {Object} conversationData - Complete conversation data from API
     * @returns {string} Formatted markdown content
     */
    function generateConversationMarkdown(conversationData, includeArtifacts = 'none', branchArtifacts = null, branchInfo = null) {
        const settings = loadSettings();
        let markdown = '';

        // Header with conversation metadata
        markdown += `# ${conversationData.name}\n`;
        
        // Project info (if available)
        if (conversationData.project) {
            markdown += `*Project:* [${conversationData.project.name}] (https://claude.ai/project/${conversationData.project.uuid})\n`;
        }
        
        markdown += `*URL:* https://claude.ai/chat/${conversationData.uuid}\n`;
        
        // Format dates properly
        markdown += `*Created:* ${formatDate(conversationData.created_at)}\n`;
        markdown += `*Updated:* ${formatDate(conversationData.updated_at)}\n`;
        markdown += `*Exported:* ${formatDate(new Date())}\n`;
        
        if (conversationData.model) {
            markdown += `*Model:* \`${conversationData.model}\`\n`;
        }
        
        markdown += `\n`;

        // Build version info for messages with alternatives
        const versionInfo = buildVersionInfo(conversationData.chat_messages);

        // Process each message
        conversationData.chat_messages.forEach(message => {
            // User canceled message
            const isCanceled = message.stop_reason === 'user_canceled';
            
            const role = message.sender === 'human' ? 'Human' : 'Claude';
            markdown += `__________\n\n`;
            markdown += `## ${role}`;
            
            // User canceled message
            if (isCanceled) {
                markdown += ` *(canceled)*`;
            }
            markdown += `\n`;
            
            markdown += `*UUID:* \`${message.uuid}\`\n`;
            markdown += `*Created:* ${formatDate(message.created_at)}\n`;
            
            // Add version info if this message has alternatives
            if (versionInfo.has(message.uuid)) {
                const info = versionInfo.get(message.uuid);
                markdown += `*Version:* ${info.version} of ${info.total}\n`;
            }
            
            markdown += `\n`;

            // For latest_per_message mode, collect latest artifact entry for each artifact ID
            let latestArtifactEntries = new Map(); // artifactId -> latest content entry
            if (includeArtifacts === 'latest_per_message') {
                // Go through content in order and keep overwriting - last one wins
                message.content.forEach(content => {
                    if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                        const artifactId = content.input.id;
                        latestArtifactEntries.set(artifactId, content);
                    }
                });
            }

            // Process message content
            message.content.forEach(content => {
                if (content.type === 'text') {
                    const processedText = processTextContent(content.text, settings.removeDoubleNewlinesFromConversation);
                    markdown += processedText + '\n\n';
                } else if (content.type === 'tool_use' && content.name === 'artifacts') {
                    const input = content.input;
                    
                    // For latest_per_message mode, only show artifact details for the latest version in this message
                    let shouldShowArtifactDetails = true;
                    if (includeArtifacts === 'latest_per_message') {
                        shouldShowArtifactDetails = latestArtifactEntries.get(input.id) === content;
                    }
                    
                    if (shouldShowArtifactDetails) {
                        if (input.title) {
                            markdown += `**Artifact Created:** ${input.title}`;
                            
                            // User canceled message
                            if (isCanceled) {
                                markdown += ` *(incomplete - generation was canceled)*`;
                            }
                            markdown += `\n`;
                        }
                        
                        markdown += `*ID:* \`${input.id}\`\n`;
                        markdown += `*Command:* \`${input.command}\`\n`;
                        
                        // Add version, branch and timestamp info if available
                        if (branchArtifacts) {
                            // Find the specific version for this operation (by timestamp if available)
                            let artifactVersion = null;
                            
                            for (const [branchId, artifactsMap] of branchArtifacts) {
                                if (artifactsMap.has(input.id)) {
                                    const versions = artifactsMap.get(input.id);
                                    
                                    if (content.stop_timestamp) {
                                        // Try to find by exact timestamp
                                        artifactVersion = versions.find(v => 
                                            v.messageUuid === message.uuid && 
                                            v.content_stop_timestamp === content.stop_timestamp
                                        );
                                    }
                                    
                                    // Fallback: find any version in this message
                                    if (!artifactVersion) {
                                        artifactVersion = versions.find(v => v.messageUuid === message.uuid);
                                    }
                                    
                                    if (artifactVersion) break;
                                }
                            }
                            
                            if (artifactVersion) {
                                // Find branch info for proper branch label  
                                const branchData = branchInfo ? branchInfo.find(b => b.branchId === artifactVersion.branchId) : null;
                                let branchLabel;
                                
                                if (branchData) {
                                    if (branchData.isMainBranch) {
                                        branchLabel = `branch${branchData.branchIndex} (main) (${artifactVersion.branchId.substring(0, 8)}...)`;
                                    } else {
                                        branchLabel = `branch${branchData.branchIndex} (${artifactVersion.branchId.substring(0, 8)}...)`;
                                    }
                                } else {
                                    branchLabel = `unknown (${artifactVersion.branchId.substring(0, 8)}...)`;
                                }
                                
                                markdown += `*Version:* ${artifactVersion.version}\n`;
                                markdown += `*Branch:* ${branchLabel}\n`;
                                markdown += `*Created:* ${formatDate(artifactVersion.content_stop_timestamp || artifactVersion.timestamp_created_at)}\n`;
                                
                                // Show change description if available
                                if (artifactVersion.changeDescription) {
                                    markdown += `*Change:* ${artifactVersion.changeDescription}\n`;
                                }
                            }
                        }
                        
                        // Include artifact content based on mode
                        if (includeArtifacts !== 'none' && branchArtifacts) {
                            // User canceled message
                            if (settings.excludeCanceledArtifacts && isCanceled) {
                                markdown += `\n*Artifact content excluded (generation was canceled)*\n`;
                            } else {
                                let artifactContent = null;
                                
                                if (includeArtifacts === 'latest_per_message') {
                                    // Find the latest artifact version in this specific message using the same logic
                                    const latestInMessage = new Map(); 
                                    message.content.forEach(c => {
                                        if (c.type === 'tool_use' && c.name === 'artifacts' && c.input && c.input.id === input.id) {
                                            latestInMessage.set(c.input.id, c);
                                        }
                                    });
                                    
                                    const latestEntry = latestInMessage.get(input.id);
                                    if (latestEntry && latestEntry === content) {
                                        // Find the corresponding artifact version
                                        for (const [branchId, artifactsMap] of branchArtifacts) {
                                            if (artifactsMap.has(input.id)) {
                                                const versions = artifactsMap.get(input.id);
                                                artifactContent = versions.find(v => 
                                                    v.content_stop_timestamp === latestEntry.stop_timestamp
                                                );
                                                if (artifactContent) break;
                                            }
                                        }
                                    }
                                } else {
                                    // Use existing logic for 'all' and 'final' modes
                                    artifactContent = findArtifactContent(input.id, message.uuid, branchArtifacts, includeArtifacts, content.stop_timestamp);
                                }
                                
                                if (artifactContent) {
                                    markdown += `\n### Artifact Content\n\n`;
                                    
                                    // Determine the language for syntax highlighting
                                    const language = getLanguageForHighlighting(artifactContent.finalType, artifactContent.finalLanguage);
                                    
                                    // Process artifact content based on settings
                                    let processedArtifactContent = artifactContent.fullContent;
                                    if (artifactContent.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) {
                                        processedArtifactContent = processArtifactContent(artifactContent.fullContent, artifactContent.finalType, true);
                                    }
                                    
                                    markdown += '```' + language + '\n';
                                    markdown += processedArtifactContent + '\n';
                                    markdown += '```\n';
                                }
                                
                            }
                        }
                        
                        markdown += `\n`;
                    }
                } else if (content.type === 'thinking') {
                    if (content.thinking) {
                        markdown += `*[Claude thinking...]*\n\n`;
                        markdown += `<details>\n<summary>Thinking process</summary>\n\n`;
                        
                        const processedThinking = processTextContent(content.thinking, settings.removeDoubleNewlinesFromConversation);
                        markdown += processedThinking + '\n\n';
                        markdown += `</details>\n\n`;
                    } else {
                        markdown += `*[Claude thinking...]*\n\n`;
                    }
                }
            });

            // Process files if present (files or files_v2)
            const attachedFiles = message.files_v2 || message.files || [];
            if (attachedFiles.length > 0) {
                attachedFiles.forEach(file => {
                    markdown += `**File:** ${file.file_name}\n`;
                    markdown += `*ID:* \`${file.file_uuid}\`\n`;
                    if (file.file_kind === 'image') {
                        markdown += `*Preview:* https://claude.ai${file.preview_url}\n`;
                    }
                    
                    markdown += `\n`;
                    
                });
            
            }

            // Process attachments if present
            if (message.attachments && message.attachments.length > 0) {
                message.attachments.forEach(attachment => {
                    markdown += `**Attachment:** ${attachment.file_name}\n`;
                    markdown += `*ID:* \`${attachment.id}\`\n\n`;
                    
                    if (!settings.excludeAttachments && attachment.extracted_content) {
                        markdown += `<details>\n\n`;
                        markdown += '```\n';
                        
                        const processedAttachment = processTextContent(attachment.extracted_content, settings.removeDoubleNewlinesFromConversation);
                        markdown += processedAttachment + '\n';
                        markdown += '```\n\n';
                        markdown += `</details>\n\n`;
                    }
                });
            }
        });

        return markdown;
    }

    /**
     * Formats artifact metadata as comments in the appropriate style
     * @param {Object} version - Version object with metadata
     * @param {string} artifactId - Artifact ID
     * @param {string} branchLabel - Branch label
     * @param {boolean} isMain - Whether this is the main branch
     * @returns {string} Formatted metadata comments
     */
    function formatArtifactMetadata(version, artifactId, branchLabel, isMain) {
        const settings = loadSettings();
        
        // Return empty string if metadata is disabled
        if (!settings.includeArtifactMetadata) {
            return '';
        }

        const metadataInfo = [
            `Artifact ID: ${artifactId}`,
            `Branch: ${branchLabel}${isMain ? ' (main)' : ''} (${version.branchId.substring(0, 8)}...)`,
            `Version: ${version.version}`,
            `Command: ${version.command}`,
            `UUID: ${version.uuid}`,
            `Created: ${formatDate(version.content_stop_timestamp)}`
        ];

        if (version.changeDescription) {
            metadataInfo.push(`Change: ${version.changeDescription}`);
        }
        
        if (version.updateInfo) {
            metadataInfo.push(`Update Info: ${version.updateInfo}`);
        }
        
        if (!isMain && version.inheritedContent && version.command === 'update') {
            metadataInfo.push(`Started from inherited content: ${version.inheritedContent.length} chars`);
        }

        // Special formatting for markdown files
        if (version.finalType === 'text/markdown') {
            let metadata = metadataInfo.map(info => `*${info}*`).join('\n') + '\n\n---\n';
            return metadata;
        }
        
        // For all other file types, use comments
        const commentStyle = getCommentStyle(version.finalType, version.finalLanguage);
        const { start, end } = commentStyle;
        
        let metadata = metadataInfo.map(info => `${start}${info}${end}`).join('\n') + '\n';
        
        // Add separator based on language
        const separators = {
            '// ': '\n// ---\n',
            '-- ': '\n-- ---\n', 
            '<!-- ': '\n<!-- --- -->\n'
        };
        
        metadata += separators[start] || '\n# ---\n';
        
        return metadata;
    }

    /**
     * Helper to count artifacts across branches
     */
    function countArtifacts(branchArtifacts) {
        return Array.from(branchArtifacts.values())
            .reduce((total, artifactsMap) => total + artifactsMap.size, 0);
    }

    /**
     * Unified export function that handles all export modes
     * @param {boolean} finalVersionsOnly - If true, exports only final artifact versions
     * @param {boolean} latestPerMessage - If true, exports only latest artifact per message
     */
    async function exportConversation(finalVersionsOnly = false, latestPerMessage = false) {
        try {
            showNotification('Fetching conversation data...', 'info');

            const conversationData = await getConversationData();
            const settings = loadSettings();

            // Extract and process artifacts from all branches
            const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);

            let conversationMarkdown;
            let shouldExportSeparateFiles = false;
            let includeMode;

            // Determine include mode for conversation markdown
            if (latestPerMessage) {
                includeMode = 'latest_per_message';
            } else if (finalVersionsOnly) {
                includeMode = 'final';
            } else {
                includeMode = 'all';
            }

            // Determine behavior based on setting
            switch (settings.artifactExportMode) {
                case 'embed':
                    // Only embed in conversation file
                    conversationMarkdown = generateConversationMarkdown(conversationData, includeMode, branchArtifacts, branchInfo);
                    shouldExportSeparateFiles = false;
                    break;
                    
                case 'files':
                    // Only separate files, no embedding (except for latestPerMessage which always shows in conversation)
                    const conversationIncludeMode = latestPerMessage ? includeMode : 'none';
                    conversationMarkdown = generateConversationMarkdown(conversationData, conversationIncludeMode, branchArtifacts, branchInfo);
                    shouldExportSeparateFiles = true;
                    break;
                    
                case 'both':
                    // Both embed and separate files
                    conversationMarkdown = generateConversationMarkdown(conversationData, includeMode, branchArtifacts, branchInfo);
                    shouldExportSeparateFiles = true;
                    break;
            }
            
            // Always download conversation file
            const conversationFilename = generateConversationFilename(conversationData);
            downloadFile(conversationFilename, conversationMarkdown);

            // Export separate artifact files if needed
            if (shouldExportSeparateFiles && branchArtifacts.size > 0) {
                let totalExported = 0;

                // For latest per message mode, build set of latest artifact timestamps
                let latestArtifactTimestamps = new Set();
                if (latestPerMessage) {
                    conversationData.chat_messages.forEach(message => {
                        const latestInMessage = new Map(); // artifactId -> latest content entry
                        
                        // Go through content in order - last artifact with each ID wins
                        message.content.forEach(content => {
                            if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                                const artifactId = content.input.id;
                                latestInMessage.set(artifactId, content);
                            }
                        });
                        
                        // Add timestamps of latest artifacts to our set
                        latestInMessage.forEach((content, artifactId) => {
                            if (content.stop_timestamp) {
                                latestArtifactTimestamps.add(content.stop_timestamp);
                            }
                        });
                    });
                }

                // Export artifacts for each branch
                branchArtifacts.forEach((artifactsMap, branchId) => {
                    const branchData = branchInfo.find(b => b.branchId === branchId);
                    const branchLabel = branchData ? branchData.branchIndex.toString() : 'unknown';
                    const isMain = branchData ? branchData.isMainBranch : false;
                    
                    artifactsMap.forEach((versions, artifactId) => {
                        let versionsToExport = versions; // Default to all versions

                        if (latestPerMessage) {
                            // Only export versions that are latest per message
                            versionsToExport = versions.filter(version => 
                                latestArtifactTimestamps.has(version.content_stop_timestamp)
                            );
                        } else if (finalVersionsOnly) {
                            // Only last version
                            versionsToExport = [versions[versions.length - 1]];
                        }

                        versionsToExport.forEach(version => {
                            // User canceled message - excludeCanceledArtifacts
                            if (settings.excludeCanceledArtifacts && version.stop_reason === 'user_canceled') {
                                return; // skip this message
                            }
                            
                            const filename = generateArtifactFilename(version, conversationData, branchLabel, isMain, artifactId);
                            
                            // Format metadata as comments
                            const metadata = formatArtifactMetadata(version, artifactId, branchLabel, isMain);
                            
                            // Process artifact content based on settings
                            let processedContent = version.fullContent;
                            if (version.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) {
                                processedContent = processArtifactContent(version.fullContent, version.finalType, true);
                            }
                            
                            // Combine metadata and content
                            const content = metadata ? metadata + '\n' + processedContent : processedContent;

                            downloadFile(filename, content);
                            totalExported++;
                        });
                    });
                });

                // Generate notification message
                let mode;
                let modeText;
                
                if (latestPerMessage) {
                    mode = 'latest per message';
                    modeText = settings.artifactExportMode === 'both' ? 
                        'latest per message embedded + latest per message as separate files' : 
                        'latest per message as separate files';
                } else if (finalVersionsOnly) {
                    mode = 'final versions';
                    modeText = settings.artifactExportMode === 'both' ? 
                        'embedded + separate files' : 
                        'separate files';
                } else {
                    mode = 'all versions';
                    modeText = settings.artifactExportMode === 'both' ? 
                        'embedded + separate files' : 
                        'separate files';
                }

                showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts as ${modeText} (${mode})`, 'success');
            } else {
                // No separate files exported
                const artifactCount = countArtifacts(branchArtifacts);
                
                if (artifactCount > 0) {
                    let mode;
                    if (latestPerMessage) {
                        mode = 'latest per message';
                    } else if (finalVersionsOnly) {
                        mode = 'final versions';
                    } else {
                        mode = 'all versions';
                    }
                    showNotification(`Export completed! Downloaded conversation with ${artifactCount} embedded artifacts (${mode})`, 'success');
                } else {
                    showNotification('Export completed! No artifacts found in conversation', 'info');
                }
            }

        } catch (error) {
            console.error('Export failed:', error);
            showNotification(`Export failed: ${error.message}`, 'error');
        }
    }

    /**
     * Exports only the conversation, with optional artifact inclusion based on settings
     */
    async function exportConversationOnly() {
        try {
            showNotification('Fetching conversation data...', 'info');

            const conversationData = await getConversationData();
            const settings = loadSettings();

            // Extract artifacts to get metadata for conversation display
            const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);
            
            const includeArtifacts = settings.includeArtifactsInConversationOnly ? 'all' : 'none';
            const conversationMarkdown = generateConversationMarkdown(conversationData, includeArtifacts, branchArtifacts, branchInfo);
            
            const conversationFilename = generateConversationFilename(conversationData);
            downloadFile(conversationFilename, conversationMarkdown);

            if (settings.includeArtifactsInConversationOnly && branchArtifacts.size > 0) {
                const artifactCount = countArtifacts(branchArtifacts);
                showNotification(`Conversation exported with ${artifactCount} embedded artifacts!`, 'success');
            } else {
                showNotification('Conversation exported successfully!', 'success');
            }

        } catch (error) {
            console.error('Export failed:', error);
            showNotification(`Export failed: ${error.message}`, 'error');
        }
    }

    // =============================================
    // INITIALIZATION
    // =============================================

    /**
     * Initializes the script and registers menu commands
     */
    function init() {
        console.log('[Claude API Exporter] Initializing...');

        // Register menu commands
        GM_registerMenuCommand('⚙️ Settings', showSettingsUI);
        GM_registerMenuCommand('📄 Export Conversation Only', exportConversationOnly);
        GM_registerMenuCommand('📁 Export Conversation + Final Artifacts', () => exportConversation(true, false));
        GM_registerMenuCommand('📁 Export Conversation + All Artifacts', () => exportConversation(false, false));
        GM_registerMenuCommand('📁 Export Conversation + Latest Artifacts Per Message', () => exportConversation(false, true));
    }

    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();