ChatGPT Message Info

Add conversation tree visualization with nodes for ChatGPT

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         ChatGPT Message Info
// @namespace    http://tampermonkey.net/
// @version      0.8.0
// @description  Add conversation tree visualization with nodes for ChatGPT
// @author       MLR
// @match        https://chatgpt.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

const LOG_PREFIX = "[ChatGPT Message Info]:";

// Escape HTML special characters
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// =============================================
// CONFIGURATION & SELECTORS
// =============================================

const CONFIG = {
    retryAttempts: 3,
    retryDelay: 500,
    debounceDelay: 300,
    streamingDebounce: 300,
    initialDelay: 1000,
    apiTimeout: 10000,
    topMargin: 200
};

const Settings = {
    defaults: {
        graph: {
            panelMode: 'push',             // 'overlay' | 'push'
            mergeAssistantChains: false    // Merge system/tool/assistant nodes into one
        }
    },

    current: null,
    load() {
        const saved = GM_getValue('chatgpt-tree-settings', null);
        this.current = saved ? JSON.parse(saved) : JSON.parse(JSON.stringify(this.defaults));
        return this.current;
    },
    save() {
        GM_setValue('chatgpt-tree-settings', JSON.stringify(this.current));
    },
    get(module, key) {
        if (!this.current) this.load();
        return this.current[module]?.[key] ?? this.defaults[module]?.[key];
    },
    set(module, key, value) {
        if (!this.current) this.load();
        if (!this.current[module]) this.current[module] = {};
        this.current[module][key] = value;
        this.save();
    }
};

const SELECTORS = {
    // ChatGPT specific selectors
    chat: {
        messageListContainer: 'main > div > div > div',
        turnBase: 'div[data-message-author-role]',
        mainContainer: 'main'
    },

    message: {
        containers: [
            'div[data-message-author-role]',
            '[data-testid^="conversation-turn"]'
        ],
        userMessage: '[data-message-author-role="user"]',
        assistantMessage: '[data-message-author-role="assistant"]'
    }
};

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

// Universal selector finder
function findElements(selectorArray, context = document) {
    for (const selector of selectorArray) {
        if (!selector || typeof selector !== 'string' || selector.trim() === '') {
            continue;
        }
        try {
            const elements = context.querySelectorAll(selector);
            if (elements.length > 0) {
                return Array.from(elements);
            }
        } catch (e) {
            console.warn(`${LOG_PREFIX} Invalid selector: ${selector}`, e);
        }
    }
    return [];
}

function findElement(selectorArray, context = document) {
    for (const selector of selectorArray) {
        if (!selector || typeof selector !== 'string' || selector.trim() === '') {
            continue;
        }
        try {
            const element = context.querySelector(selector);
            if (element) return element;
        } catch (e) {
            console.warn(`${LOG_PREFIX} Invalid selector: ${selector}`, e);
        }
    }
    return null;
}

// =============================================
// API FUNCTIONS
// =============================================

// Gets conversation ID from URL
function getConversationId() {
    const match = window.location.pathname.match(/\/c\/([^/?]+)/);
    return match ? match[1] : null;
}

// Gets session info
async function getSession() {
    const response = await fetch("https://chatgpt.com/api/auth/session");
    return await response.json();
}

// Retrieves bearer token for API authentication
async function getBearerToken() {
    try {
        const session = await getSession();
        if (!session.accessToken) {
            throw new Error('No access token found. Please log in to ChatGPT.');
        }
        return session.accessToken;
    } catch (error) {
        console.error(`${LOG_PREFIX} Failed to get bearer token:`, error);
        throw new Error(`Failed to get bearer token: ${error.message}`);
    }
}

// Fetches conversation data from ChatGPT API
async function fetchConversationData(conversationId) {
    try {
        console.log(`${LOG_PREFIX} Fetching conversation data for: ${conversationId}`);

        const token = await getBearerToken();

        const response = await fetch(`https://chatgpt.com/backend-api/conversation/${conversationId}`, {
            headers: {
                'Accept': '*/*',
                'Authorization': `Bearer ${token}`
            }
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();
        console.log(`${LOG_PREFIX} Conversation data received:`, data);
        return data;
    } catch (error) {
        console.error(`${LOG_PREFIX} Failed to fetch conversation:`, error);
        return null;
    }
}

// =============================================
// TREE STRUCTURE FUNCTIONS
// =============================================

// Helper: Find and merge initial system chain (nodes without request_id)
function mergeInitialSystemChain(nodes, nodeMap, mapping, processed) {
    const nodesToRemove = [];
    const nodesToAdd = [];

    // Special case: merge initial system nodes (no request_id at start)
    // These are client-created-root and system instructions without a request ID
    const rootNode = nodes.find(n => !n.parentUuid);
    if (!rootNode) return { nodesToRemove, nodesToAdd };

    const initialChain = [];
    let current = rootNode;

    // Collect nodes while they have no request_id and are not user
    while (current && !current.metadata?.request_id && current.role !== 'user') {
        initialChain.push(current);
        processed.add(current.uuid);

        if (current.children.length > 0) {
            const nextUuid = current.children[0];  // Only first children!!!
            current = nodeMap.get(nextUuid);
        } else {
            break;
        }
    }

    // If we have multiple nodes, merge them
    if (initialChain.length > 1) {
        console.log(`${LOG_PREFIX} Found initial chain (no ID): ${initialChain.map(n => `${n.nodeIndex}:${n.role}`).join(' → ')}`);

        const lastNode = initialChain[initialChain.length - 1];

        const mergedNode = {
            uuid: lastNode.uuid,
            parentUuid: null,
            children: lastNode.children,
            role: 'root',
            content: initialChain.map(n => {
                const c = n.content || '';
                return c.trim() ? `${n.nodeIndex}:\n${c}` : '';
            }).filter(Boolean).join('\n__\n'),
            createTime: lastNode.createTime,
            metadata: lastNode.metadata || {},
            nodeIndex: 0,
            index: 0,
            chainNodes: initialChain.map(n => ({
                nodeIndex: n.nodeIndex,
                uuid: n.uuid,
                role: n.role || 'N/A',
                content: n.content,
                metadata: n.metadata,
                parentUuid: n.parentUuid,
                children: n.children,
                createTime: n.createTime,
                rawData: mapping[n.uuid]
            }))
        };

        nodesToRemove.push(...initialChain);
        nodesToAdd.push(mergedNode);
    }

    return { nodesToRemove, nodesToAdd };
}

// Helper: Find chains by request_id/turn_exchange_id and create merged nodes
function findAndMergeChainsByIds(nodes, nodeMap, mapping, processed) {
    const nodesToRemove = [];
    const nodesToAdd = [];

    for (const node of nodes) {
        if (processed.has(node.uuid)) continue;
        // We do NOT merge user nodes, even if they have a request_id
        if (node.role === 'user') continue;

        // Start chain only if node has at least one ID
        const hasReqId = node.metadata?.request_id;
        const hasTurnId = node.metadata?.turn_exchange_id;
        if (!hasReqId && !hasTurnId) continue; // Skip nodes without ID (already handled roots above, or dangling nodes)

        const chain = [];
        let current = node;
        // Set of IDs that belong to this chain
        const chainIds = new Set();
        if (hasReqId) chainIds.add(hasReqId);
        if (hasTurnId) chainIds.add(hasTurnId);

        // Follow the chain while request_id or turn_exchange_id matches and role is not user
        while (current && current.role !== 'user') {
            const currReqId = current.metadata?.request_id;
            const currTurnId = current.metadata?.turn_exchange_id;
            let matches = false;
            if (currReqId && chainIds.has(currReqId)) matches = true;
            if (currTurnId && chainIds.has(currTurnId)) matches = true;
            if (chain.length === 0) matches = true;

            // Include intermediate nodes without IDs if next child matches
            if (!matches && chain.length > 0 && !currReqId && !currTurnId) {
                if (current.children.length > 0) {
                    const nextNode = nodeMap.get(current.children[0]);
                    if (nextNode) {
                        const nextReqId = nextNode.metadata?.request_id;
                        const nextTurnId = nextNode.metadata?.turn_exchange_id;
                        if ((nextReqId && chainIds.has(nextReqId)) || (nextTurnId && chainIds.has(nextTurnId))) {
                            matches = true;
                        }
                    }
                }
            }

            if (matches) {
                chain.push(current);
                processed.add(current.uuid);
                if (currReqId) chainIds.add(currReqId);
                if (currTurnId) chainIds.add(currTurnId);

                if (current.children.length > 0) {
                    current = nodeMap.get(current.children[0]);
                } else {
                    break;
                }
            } else {
                break;
            }
        }

        // If chain has more than 1 node, merge it
        if (chain.length > 1) {
            console.log(`${LOG_PREFIX} Found chain (IDs: ${Array.from(chainIds).join(', ')}): ${chain.map(n => `${n.nodeIndex}:${n.role}`).join(' → ')}`);

            const firstNode = chain[0];
            const lastNode = chain[chain.length - 1];

            // Merged nodes need request_id and turn_exchange_id to be grouped with User node during sort
            const finalMetadata = { ...lastNode.metadata };
            if (!finalMetadata.request_id) {
                const nodeWithReqId = chain.find(n => n.metadata?.request_id);
                if (nodeWithReqId) finalMetadata.request_id = nodeWithReqId.metadata.request_id;
            }
            if (!finalMetadata.turn_exchange_id) {
                const nodeWithTurnId = chain.find(n => n.metadata?.turn_exchange_id);
                if (nodeWithTurnId) finalMetadata.turn_exchange_id = nodeWithTurnId.metadata.turn_exchange_id;
            }

            // Tool results often have null createTime, causing the merged node to sort to index 0 (top of list).
            // We use the first node's time as a fallback, so it gets the correct chronological number.
            const finalCreateTime = lastNode.createTime || firstNode.createTime;

            const mergedNode = {
                uuid: lastNode.uuid,
                parentUuid: firstNode.parentUuid,
                children: lastNode.children,
                role: 'assistant',
                content: chain.map(n => {
                    const c = n.content || '';
                    return c.trim() ? `${n.nodeIndex}:\n${c}` : '';
                }).filter(Boolean).join('\n__\n'),
                createTime: finalCreateTime,
                metadata: finalMetadata,
                nodeIndex: firstNode.nodeIndex,
                index: lastNode.index,
                chainNodes: chain.map(n => ({
                    nodeIndex: n.nodeIndex,
                    uuid: n.uuid,
                    role: n.role,
                    content: n.content,
                    metadata: n.metadata,
                    parentUuid: n.parentUuid,
                    children: n.children,
                    createTime: n.createTime,
                    rawData: mapping[n.uuid]
                }))
            };

            nodesToRemove.push(...chain);
            nodesToAdd.push(mergedNode);
        }
    }

    return { nodesToRemove, nodesToAdd };
}

// Helper: Collect orphaned children from nodes that will be removed
function collectOrphanedChildren(nodesToRemove, nodesToAdd) {
    const childrenToAdopt = new Map();
    nodesToRemove.forEach(removedNode => {
        for (const mergedNode of nodesToAdd) {
            const isInMerged = mergedNode.chainNodes.some(cn => cn.uuid === removedNode.uuid);
            if (isInMerged) {
                if (!childrenToAdopt.has(mergedNode.uuid)) {
                    childrenToAdopt.set(mergedNode.uuid, []);
                }
                removedNode.children.forEach(childUuid => {
                    const isAlsoInChain = mergedNode.chainNodes.some(cn => cn.uuid === childUuid);
                    if (!isAlsoInChain && !childrenToAdopt.get(mergedNode.uuid).includes(childUuid)) {
                        childrenToAdopt.get(mergedNode.uuid).push(childUuid);
                    }
                });
                break;
            }
        }
    });

    return childrenToAdopt;
}

// Helper: Remove old nodes from arrays and map
function removeOldNodes(nodes, nodeMap, nodesToRemove) {
    for (const node of nodesToRemove) {
        const idx = nodes.indexOf(node);
        if (idx !== -1) {
            nodes.splice(idx, 1);
        }
        nodeMap.delete(node.uuid);
    }
}

// Helper: Add merged nodes and adopt their orphaned children
function addMergedNodesWithChildren(nodes, nodeMap, nodesToAdd, childrenToAdopt) {
    for (const node of nodesToAdd) {
        nodes.push(node);
        nodeMap.set(node.uuid, node);

        // Adopt orphaned children
        if (childrenToAdopt.has(node.uuid)) {
            const adoptedChildren = childrenToAdopt.get(node.uuid);
            adoptedChildren.forEach(childUuid => {
                if (!node.children.includes(childUuid)) {
                    node.children.push(childUuid);
                    const childNode = nodes.find(n => n.uuid === childUuid);
                    if (childNode) {
                        childNode.parentUuid = node.uuid;
                    }
                }
            });
        }

        // Update parent's children array
        if (node.parentUuid) {
            const parent = nodeMap.get(node.parentUuid);
            if (parent) {
                node.parent = parent;
                const oldFirstUuid = node.chainNodes[0].uuid;
                const childIdx = parent.children.indexOf(oldFirstUuid);
                if (childIdx !== -1) {
                    parent.children[childIdx] = node.uuid;
                }
            }
        }
    }
}

// Helper: Merge remaining unmerged tool nodes into their parent
function mergeUnmergedToolNodes(nodes, nodeMap, mapping) {
    const unmergedToolNodes = nodes.filter(n =>
        n.role === 'tool' && !n.chainNodes && n.parentUuid
    );

    if (unmergedToolNodes.length === 0) return;

    console.log(`${LOG_PREFIX} Found ${unmergedToolNodes.length} unmerged tool nodes to merge`);
    const nodesToRemove = [];

    for (const toolNode of unmergedToolNodes) {
        const parentNode = nodeMap.get(toolNode.parentUuid);
        if (!parentNode || parentNode.role === 'user') continue;

        // Create chainNodes if parent doesn't have them
        if (!parentNode.chainNodes) {
            parentNode.chainNodes = [{
                nodeIndex: parentNode.nodeIndex,
                uuid: parentNode.uuid,
                role: parentNode.role,
                content: parentNode.content,
                metadata: parentNode.metadata,
                parentUuid: parentNode.parentUuid,
                children: parentNode.children,
                createTime: parentNode.createTime,
                rawData: mapping[parentNode.uuid]
            }];
        }

        // Add tool node to chainNodes
        parentNode.chainNodes.push({
            nodeIndex: toolNode.nodeIndex,
            uuid: toolNode.uuid,
            role: toolNode.role,
            content: toolNode.content,
            metadata: toolNode.metadata,
            parentUuid: toolNode.parentUuid,
            children: toolNode.children,
            createTime: toolNode.createTime,
            rawData: mapping[toolNode.uuid]
        });

        // Transfer children
        for (const childUuid of toolNode.children) {
            if (!parentNode.children.includes(childUuid)) {
                parentNode.children.push(childUuid);
                const childNode = nodeMap.get(childUuid);
                if (childNode) {
                    childNode.parentUuid = parentNode.uuid;
                    childNode.parent = parentNode;
                }
            }
        }

        // Remove tool from parent's children
        parentNode.children = parentNode.children.filter(uuid => uuid !== toolNode.uuid);
        nodesToRemove.push(toolNode);
    }

    // Rebuild content for modified parents
    const modifiedParents = new Set(unmergedToolNodes.map(n => n.parentUuid));
    for (const parentUuid of modifiedParents) {
        const parentNode = nodeMap.get(parentUuid);
        if (parentNode && parentNode.chainNodes) {
            parentNode.content = parentNode.chainNodes.map(n => {
                const c = n.content || '';
                return c.trim() ? `${n.nodeIndex}:\n${c}` : '';
            }).filter(Boolean).join('\n__\n');
        }
    }
    // Remove tool nodes from array (but NOT from nodeMap - needed for mainBranch tracing)
    for (const toolNode of nodesToRemove) {
        const idx = nodes.indexOf(toolNode);
        if (idx !== -1) {
            nodes.splice(idx, 1);
        }
    }
    console.log(`${LOG_PREFIX} Merged and removed ${nodesToRemove.length} tool nodes`);
}

// Helper: Sort and reindex nodes by chronological order
function sortAndReindexNodes(nodes, nodeMap) {
    const sortedNodes = [...nodes].sort((a, b) => {
        // Use parent time for nodes without createTime
        let timeA = a.createTime;
        let timeB = b.createTime;
        if (!timeA && a.parentUuid) {
            const parentA = nodeMap.get(a.parentUuid);
            timeA = parentA?.createTime || 0;
        } else {
            timeA = timeA || 0;
        }
        if (!timeB && b.parentUuid) {
            const parentB = nodeMap.get(b.parentUuid);
            timeB = parentB?.createTime || 0;
        } else {
            timeB = timeB || 0;
        }
        const reqA = a.metadata?.request_id;
        const reqB = b.metadata?.request_id;
        const turnA = a.metadata?.turn_exchange_id;
        const turnB = b.metadata?.turn_exchange_id;

        // 1. User + Assistant pair grouping
        const isSameGroup = (reqA && reqA === reqB) || (turnA && turnA === turnB);
        if (isSameGroup && a.role === 'user' && b.role !== 'user') {
            return -1;
        }
        if (isSameGroup && b.role === 'user' && a.role !== 'user') {
            return 1;
        }

        // 2. Siblings order
        // Ensure v1 comes before v2, v2 before v3, etc.
        // This cases where v2 has an earlier timestamp than v1 due to API quirks.
        if (a.parent && a.parent === b.parent) {
            const siblings = a.parent.children;
            const indexA = siblings.indexOf(a.uuid);
            const indexB = siblings.indexOf(b.uuid);
            // If both are found in parent's children array, sort by that index
            if (indexA !== -1 && indexB !== -1) {
                return indexA - indexB;
            }
        }

        // 3. Default: Sort by time
        return timeA - timeB;
    });

    // Assign indices chronologically
    sortedNodes.forEach((node, index) => {
        // Save old nodeIndex as unmerged for non-chainNodes (User nodes)
        if (!node.chainNodes) {
            node.unmergedIndex = node.nodeIndex;
        }
        node.nodeIndex = index;
        node.index = index;
    });
}

function mergeAssistantChains(nodes, nodeMap, mapping) {
    console.log(`${LOG_PREFIX} Starting merge, total nodes: ${nodes.length}`);

    const processed = new Set();
    let allNodesToRemove = [];
    let allNodesToAdd = [];

    // Step 1: Merge initial system chain (root nodes without request_id)
    const initialResult = mergeInitialSystemChain(nodes, nodeMap, mapping, processed);
    allNodesToRemove.push(...initialResult.nodesToRemove);
    allNodesToAdd.push(...initialResult.nodesToAdd);

    // Step 2: Find and merge chains by request_id/turn_exchange_id
    const chainResult = findAndMergeChainsByIds(nodes, nodeMap, mapping, processed);
    allNodesToRemove.push(...chainResult.nodesToRemove);
    allNodesToAdd.push(...chainResult.nodesToAdd);

    console.log(`${LOG_PREFIX} Removing ${allNodesToRemove.length} nodes, adding ${allNodesToAdd.length} merged nodes`);

    // Step 3: Collect orphaned children BEFORE removing nodes
    const childrenToAdopt = collectOrphanedChildren(allNodesToRemove, allNodesToAdd);

    // Step 4: Remove old nodes
    removeOldNodes(nodes, nodeMap, allNodesToRemove);

    // Step 5: Add merged nodes and adopt children
    addMergedNodesWithChildren(nodes, nodeMap, allNodesToAdd, childrenToAdopt);

    // Step 6: Merge remaining unmerged tool nodes
    mergeUnmergedToolNodes(nodes, nodeMap, mapping);

    // Step 7: Sort and reindex by chronological order
    sortAndReindexNodes(nodes, nodeMap);

    console.log(`${LOG_PREFIX} Merge complete, final nodes: ${nodes.length}`);
}

// Builds tree structure from conversation mapping with branch detection
function buildTreeStructure(conversationData) {
    if (!conversationData || !conversationData.mapping) {
        console.error(`${LOG_PREFIX} Invalid conversation data`);
        return null;
    }

    const mapping = conversationData.mapping;
    const nodes = [];
    const nodeMap = new Map();
    const messageToBranch = new Map();

    // First pass: create all nodes
    for (const [uuid, data] of Object.entries(mapping)) {
        const message = data.message;
        const role = message?.author?.role || 'N/A';
        const content = extractMessageContent(message);
        const node = {
            uuid: uuid,
            parentUuid: data.parent || null,
            children: data.children || [],
            role: role,
            content: content,
            createTime: message?.create_time || null,
            metadata: message?.metadata || {},
            nodeIndex: nodes.length, // Add index for displaying numbers in node
            index: message?.create_time || 0 // Use create_time as index for sorting
        };
        nodes.push(node);
        nodeMap.set(uuid, node);
    }

    // Second pass: establish parent-child relationships
    for (const node of nodes) {
        if (node.parentUuid && nodeMap.has(node.parentUuid)) {
            node.parent = nodeMap.get(node.parentUuid);
        }
    }

    // Third pass: restructure User→User chains after GPT responses
    // If User node has both GPT child and User child, User child should be child of GPT
    for (const node of nodes) {
        if (node.role !== 'user' || node.children.length < 2) continue;
        const children = node.children.map(uuid => nodeMap.get(uuid)).filter(Boolean);
        // Find GPT child with same IDs (pair)
        const gptPairChild = children.find(child => {
            if (child.role === 'user') return false;

            const parentReqId = node.metadata?.request_id;
            const parentTurnId = node.metadata?.turn_exchange_id;
            const childReqId = child.metadata?.request_id;
            const childTurnId = child.metadata?.turn_exchange_id;

            return (parentReqId && parentReqId === childReqId) ||
                   (parentTurnId && parentTurnId === childTurnId);
        });
        // Find User child with different IDs (next message)
        const userNextChild = children.find(child => {
            if (child.role !== 'user') return false;

            const parentReqId = node.metadata?.request_id;
            const parentTurnId = node.metadata?.turn_exchange_id;
            const childReqId = child.metadata?.request_id;
            const childTurnId = child.metadata?.turn_exchange_id;

            const sameIds = (parentReqId && parentReqId === childReqId) ||
                           (parentTurnId && parentTurnId === childTurnId);

            return !sameIds;
        });
        // Reconnect: User next should be child of GPT pair
        if (gptPairChild && userNextChild) {
            // Remove userNext from current node's children
            node.children = node.children.filter(uuid => uuid !== userNextChild.uuid);
            // Add userNext as child of GPT
            if (!gptPairChild.children.includes(userNextChild.uuid)) {
                gptPairChild.children.push(userNextChild.uuid);
            }
            // Update userNext's parent
            userNextChild.parentUuid = gptPairChild.uuid;
            userNextChild.parent = gptPairChild;
        }
    }

    // Merge assistant chains if setting enabled
    const mergeEnabled = Settings.get('graph', 'mergeAssistantChains');
    if (mergeEnabled) {
        mergeAssistantChains(nodes, nodeMap, mapping);
    } else {
        // Even without merging, apply the same chronological sorting to ensure correct branch numbering based on time, not DFS order
        const sortedNodes = [...nodes].sort((a, b) => {
            const timeA = a.createTime || 0;
            const timeB = b.createTime || 0;
            const reqA = a.metadata?.request_id;
            const reqB = b.metadata?.request_id;
            const turnA = a.metadata?.turn_exchange_id;
            const turnB = b.metadata?.turn_exchange_id;

            // 1. User + Assistant pair grouping
            // If nodes share the same request_id or turn_exchange_id (User + GPT pair)
            // force User to come before Assistant, ignoring timestamps.
            // Handle ChatGPT API quirk: User message create_time can be later than the Assistant's response.
            // We check turn_exchange_id because merged nodes might only carry this ID.
            const isSameGroup = (reqA && reqA === reqB) || (turnA && turnA === turnB);
            if (isSameGroup) {
                if (a.role === 'user' && b.role !== 'user') return -1; // User first
                if (b.role === 'user' && a.role !== 'user') return 1;  // User first
            }

            // 2. Siblings (same parent) order
            // Ensure v1 comes before v2, v2 before v3, etc.
            // This fixes cases where v2 has an earlier timestamp than v1 due to API quirks.
            if (a.parent && a.parent === b.parent) {
                const siblings = a.parent.children; // Array of UUIDs
                const indexA = siblings.indexOf(a.uuid);
                const indexB = siblings.indexOf(b.uuid);

                // If both are found in parent's children array, sort by that index
                if (indexA !== -1 && indexB !== -1) {
                    return indexA - indexB; // Sort by array index (0, 1, 2...)
                }
            }

            // 3. Default: Sort by time
            return timeA - timeB;
        });

        // Assign indices chronologically
        sortedNodes.forEach((node, index) => {
            // DON'T change nodeIndex in non-merged mode - keep API order!
            // Only change 'index' for correct branch numbering by time
            node.index = index;
        });
    }

    // Find root node and main branch path
    const rootNode = nodes.find(n => !n.parentUuid);

    // Trace main branch from current_node back to root
    const mainBranchUuids = new Set();
    let currentUuid = conversationData.current_node;
    while (currentUuid) {
        mainBranchUuids.add(currentUuid);
        const node = nodeMap.get(currentUuid);
        currentUuid = node?.parentUuid;
    }

    // Find all branch points (nodes with multiple children)
    const branchStartPoints = [];
    for (const node of nodes) {
        if (node.children.length > 1) {
            // Sort children by index
            const childNodes = node.children
                .map(uuid => nodeMap.get(uuid))
                .filter(Boolean)
                .sort((a, b) => a.index - b.index);

            // All children except first are new branch starts
            for (let i = 1; i < childNodes.length; i++) {
                const child = childNodes[i];
                const parentReqId = node.metadata?.request_id;
                const parentTurnId = node.metadata?.turn_exchange_id;
                const childReqId = child.metadata?.request_id;
                const childTurnId = child.metadata?.turn_exchange_id;
                const isSameGroup =
                    (parentReqId && parentReqId === childReqId) ||
                    (parentTurnId && parentTurnId === childTurnId);
                if (isSameGroup || (node.role === 'user' && child.role === 'user')) {
                    continue;
                }
                branchStartPoints.push({
                    node: child,
                    index: child.index
                });
            }
        }
    }

    // Sort branch start points by index
    branchStartPoints.sort((a, b) => a.index - b.index);

    // Assign branch numbers to start points
    const nodeToBranchNumber = new Map();
    branchStartPoints.forEach((p, i) => {
        nodeToBranchNumber.set(p.node.uuid, i + 2); // Start from 2
    });

    // Recursively assign branches
    function assignBranch(nodeUuid, currentBranch) {
        const node = nodeMap.get(nodeUuid);
        if (!node) return;

        const isMainBranch = mainBranchUuids.has(node.uuid);
        messageToBranch.set(node.uuid, {
            branchIndex: currentBranch,
            isMainBranch
        });

        if (node.children.length === 0) return;

        // Sort children by index
        const childNodes = node.children
            .map(uuid => nodeMap.get(uuid))
            .filter(Boolean)
            .sort((a, b) => a.index - b.index);

        // First child continues current branch
        assignBranch(childNodes[0].uuid, currentBranch);

        // Other children start new branches
        for (let i = 1; i < childNodes.length; i++) {
            const child = childNodes[i];
            const newBranch = nodeToBranchNumber.get(child.uuid) || currentBranch;
            assignBranch(child.uuid, newBranch);
        }
    }

    // Start assignment from root with branch 1
    if (rootNode) {
        assignBranch(rootNode.uuid, 1);
    }

    console.log(`${LOG_PREFIX} Built tree with ${nodes.length} nodes, ${branchStartPoints.length} branch points`);
    console.log(`${LOG_PREFIX} Main branch has ${mainBranchUuids.size} nodes`);

    return {
        nodes: nodes,
        nodeMap: nodeMap,
        rootNode: rootNode,
        currentLeaf: conversationData.current_node,
        messageToBranch: messageToBranch,
        mainBranchUuids: mainBranchUuids,
        activePath: mainBranchUuids,  // activePath = mainBranchUuids (same thing)
        rawMapping: mapping  // Save original mapping for Raw JSON
    };
}

// Extracts text content from message
function extractMessageContent(message) {
    if (!message || !message.content) return '';

    const content = message.content;
    let extractedText = '';

    // Handle text property (used for 'code' content_type)
    if (content.text && typeof content.text === 'string') {
        extractedText = content.text;
    }
    // Handle parts array (standard text/multimodal)
    else if (content.parts && Array.isArray(content.parts)) {
        // Extract text parts
        extractedText = content.parts
            .filter(p => typeof p === 'string')
            .join('\n')
            .trim();
    }
    // Handle direct string content
    else if (typeof content === 'string') {
        extractedText = content;
    }

    // Add image_gen_title from metadata if exists
    if (message.metadata && message.metadata.image_gen_title) {
        const title = message.metadata.image_gen_title;
        extractedText = extractedText ? `${extractedText}\n\n[Image title: ${title}]` : `[Image title: ${title}]`;
    }

    return extractedText;
}

// =============================================
// TREE VISUALIZATION
// =============================================

class ConversationTreePanel {
    constructor() {
        this.isOpen = false;
        this.treeData = null;
        this.canvas = null;
        this.ctx = null;
        this.container = null;
        this.currentHighlightedNode = null;
        this.canvasOffset = { x: 0, y: 0 };
        this.canvasScale = 1;
        this.isDragging = false;
        this.dragStart = { x: 0, y: 0 };
        this.nodePositions = new Map();
        this.isResizing = false;
        this.panelWidth = 400;

        Settings.load();
    }

    async initialize() {
        console.log(`${LOG_PREFIX} Initializing tree panel`);
        this.createPanelStructure();
        this.createToggleButton();
        await this.loadConversationTree();
    }

    createPanelStructure() {
        // Create wrapper
        const wrapper = document.createElement('div');
        wrapper.id = 'chatgpt-tree-panel-wrapper';
        wrapper.className = 'tree-panel-wrapper';
        wrapper.style.display = 'none';

        // Create panel
        const panel = document.createElement('div');
        panel.className = 'tree-panel';

        // Create header
        const header = document.createElement('div');
        header.className = 'tree-panel-header';
        header.innerHTML = `
            <div class="tree-header-content">
                <h3>Conversation Tree</h3>
                <div class="tree-header-controls">
                    <button class="tree-control-btn" id="tree-settings-btn" title="Settings">⚙️</button>
                    <button class="tree-control-btn" id="tree-reset-btn" title="Reset zoom">
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
                            <path d="M21 3v5h-5"/>
                        </svg>
                    </button>
                    <button class="tree-control-btn" id="tree-refresh-btn" title="Refresh">
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
                        </svg>
                    </button>
                    <button class="tree-control-btn" id="tree-close-btn" title="Close">
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <line x1="18" y1="6" x2="6" y2="18"></line>
                            <line x1="6" y1="6" x2="18" y2="18"></line>
                        </svg>
                    </button>
                </div>
            </div>
        `;

        // Create content container
        const content = document.createElement('div');
        content.className = 'tree-panel-content';

        // Create canvas
        this.canvas = document.createElement('canvas');
        this.canvas.className = 'tree-canvas';
        this.canvas.style.cssText = 'display: block; cursor: grab;';

        content.appendChild(this.canvas);

        panel.appendChild(header);
        panel.appendChild(content);

        // Resizer handle
        const resizer = document.createElement('div');
        resizer.className = 'tree-resizer-group';
        resizer.innerHTML = `
            <div class="tree-resizer-line"></div>
            <div class="tree-resizer-handle"></div>
        `;
        resizer.addEventListener('mousedown', (e) => this.startResizing(e));
        wrapper.appendChild(resizer);

        wrapper.appendChild(panel);

        document.body.appendChild(wrapper);
        this.container = wrapper;

        // Setup canvas event handlers
        this.setupCanvasHandlers();

        // Add event listeners
        document.getElementById('tree-close-btn').addEventListener('click', () => this.toggle());
        document.getElementById('tree-refresh-btn').addEventListener('click', () => this.refresh());
        document.getElementById('tree-settings-btn').addEventListener('click', () => this.openSettings());
        document.getElementById('tree-reset-btn').addEventListener('click', () => this.resetZoom());
    }

    resetZoom() {
        console.log(`${LOG_PREFIX} Resetting zoom`);
        this.canvasOffset = { x: 0, y: 0 };
        this.canvasScale = 1;
        if (this.treeData) {
            this.renderTree();
        }
    }

    setupCanvasHandlers() {
        // Click handler for node selection
        this.canvas.addEventListener('click', (e) => {
            if (this.isDragging) return;

            const rect = this.canvas.getBoundingClientRect();
            const x = (e.clientX - rect.left - this.canvasOffset.x) / this.canvasScale;
            const y = (e.clientY - rect.top - this.canvasOffset.y) / this.canvasScale;

            const node = this.findNodeAtPosition(x, y);
            if (node) {
                this.onNodeClick(node);
            }
        });

        // Mouse move for dragging and hover
        this.canvas.addEventListener('mousemove', (e) => {
            if (this.isDragging) {
                const dx = e.clientX - this.dragStart.x;
                const dy = e.clientY - this.dragStart.y;
                this.canvasOffset.x += dx;
                this.canvasOffset.y += dy;
                this.dragStart = { x: e.clientX, y: e.clientY };
                this.renderTree();
                return;
            }

            const rect = this.canvas.getBoundingClientRect();
            const x = (e.clientX - rect.left - this.canvasOffset.x) / this.canvasScale;
            const y = (e.clientY - rect.top - this.canvasOffset.y) / this.canvasScale;

            const node = this.findNodeAtPosition(x, y);
            if (node) {
                this.canvas.style.cursor = 'pointer';
            } else {
                this.canvas.style.cursor = this.isDragging ? 'grabbing' : 'grab';
            }
        });

        // Mouse down - start dragging
        this.canvas.addEventListener('mousedown', (e) => {
            const rect = this.canvas.getBoundingClientRect();
            const x = (e.clientX - rect.left - this.canvasOffset.x) / this.canvasScale;
            const y = (e.clientY - rect.top - this.canvasOffset.y) / this.canvasScale;

            const node = this.findNodeAtPosition(x, y);
            if (!node) {
                this.isDragging = true;
                this.dragStart = { x: e.clientX, y: e.clientY };
                this.canvas.style.cursor = 'grabbing';
            }
        });

        // Mouse up - stop dragging
        this.canvas.addEventListener('mouseup', () => {
            this.isDragging = false;
            this.canvas.style.cursor = 'grab';
        });

        // Mouse leave - stop dragging
        this.canvas.addEventListener('mouseleave', () => {
            this.isDragging = false;
            this.canvas.style.cursor = 'grab';
        });

        // Zoom with mouse wheel
        this.canvas.addEventListener('wheel', (e) => {
            e.preventDefault();

            const rect = this.canvas.getBoundingClientRect();
            const mouseX = e.clientX - rect.left;
            const mouseY = e.clientY - rect.top;

            const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
            const newScale = Math.max(0.3, Math.min(3, this.canvasScale * zoomDelta));

            // Zoom to cursor position
            const scaleDiff = newScale - this.canvasScale;
            this.canvasOffset.x -= (mouseX - this.canvasOffset.x) * (scaleDiff / this.canvasScale);
            this.canvasOffset.y -= (mouseY - this.canvasOffset.y) * (scaleDiff / this.canvasScale);

            this.canvasScale = newScale;
            this.renderTree();
        });
    }

    createToggleButton() {
        const button = document.createElement('button');
        button.id = 'chatgpt-tree-toggle-btn';
        button.className = 'tree-toggle-btn';
        button.title = 'Show Conversation Tree';
        button.innerHTML = `
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <circle cx="12" cy="5" r="2"></circle>
                <circle cx="6" cy="12" r="2"></circle>
                <circle cx="18" cy="12" r="2"></circle>
                <circle cx="12" cy="19" r="2"></circle>
                <line x1="12" y1="7" x2="12" y2="17"></line>
                <line x1="12" y1="7" x2="6" y2="10"></line>
                <line x1="12" y1="7" x2="18" y2="10"></line>
            </svg>
        `;

        button.addEventListener('click', () => this.toggle());
        document.body.appendChild(button);
    }

    async loadConversationTree() {
        const conversationId = getConversationId();
        if (!conversationId) {
            console.warn(`${LOG_PREFIX} No conversation ID found`);
            this.showError('No conversation found');
            return;
        }
        const data = await fetchConversationData(conversationId);
        if (!data) {
            console.error(`${LOG_PREFIX} Failed to load conversation data`);
            this.showError('Failed to load conversation');
            return;
        }
        this.treeData = buildTreeStructure(data);
        if (this.treeData) {
            this.renderTree();
        } else {
            this.showError('Failed to build tree structure');
        }
    }

    showError(message) {
        if (!this.canvas) {
            console.error(`${LOG_PREFIX} No canvas for error display`);
            return;
        }

        const content = this.container.querySelector('.tree-panel-content');
        const rect = content.getBoundingClientRect();
        const width = rect.width;
        const height = rect.height;

        // Set canvas size
        const dpr = window.devicePixelRatio || 1;
        this.canvas.width = width * dpr;
        this.canvas.height = height * dpr;
        this.canvas.style.width = `${width}px`;
        this.canvas.style.height = `${height}px`;

        this.ctx = this.canvas.getContext('2d');
        this.ctx.scale(dpr, dpr);

        // Clear and draw error
        this.ctx.fillStyle = '#0f172a';
        this.ctx.fillRect(0, 0, width, height);

        this.ctx.fillStyle = '#ef4444';
        this.ctx.font = '14px sans-serif';
        this.ctx.textAlign = 'center';
        this.ctx.fillText(message, width / 2, height / 2);
    }

    renderTree() {
        if (!this.treeData || !this.canvas) {
            console.error(`${LOG_PREFIX} No tree data or canvas to render`);
            return;
        }

        const content = this.container.querySelector('.tree-panel-content');
        const rect = content.getBoundingClientRect();

        const width = rect.width;
        const height = rect.height;

        // Set canvas size with device pixel ratio
        const dpr = window.devicePixelRatio || 1;
        this.canvas.width = width * dpr;
        this.canvas.height = height * dpr;
        this.canvas.style.width = `${width}px`;
        this.canvas.style.height = `${height}px`;

        this.ctx = this.canvas.getContext('2d');
        this.ctx.scale(dpr, dpr);

        // Clear canvas
        this.ctx.fillStyle = '#0f172a';
        this.ctx.fillRect(0, 0, width, height);

        // Apply transformation
        this.ctx.save();
        this.ctx.translate(this.canvasOffset.x, this.canvasOffset.y);
        this.ctx.scale(this.canvasScale, this.canvasScale);

        // Calculate layout
        this.calculateLayout();

        // Draw edges first
        this.drawEdges();

        // Draw nodes
        this.drawNodes();

        this.ctx.restore();

        console.log(`${LOG_PREFIX} Tree rendered with ${this.treeData.nodes.length} nodes`);
    }

    calculateLayout() {
        const { nodes, rootNode } = this.treeData;
        const nodeSize = 80; // Increased for branches
        const levelHeight = 120;

        this.nodePositions.clear();

        // Recursively build tree with branches
        const buildLayout = (node, level, xOffset) => {
            if (!node) return { width: 0, minX: 0, maxX: 0 };

            // Position of current node
            const y = level * levelHeight + 50;

            // Get children
            const children = node.children
                .map(childId => this.treeData.nodeMap.get(childId))
                .filter(Boolean);

            if (children.length === 0) {
                // Leaf node
                const x = xOffset;
                this.nodePositions.set(node.uuid, {
                    x,
                    y,
                    node,
                    radius: 20
                });
                return { width: nodeSize, minX: x, maxX: x };
            }

            // Recursively place children
            let currentX = xOffset;
            let minX = Infinity;
            let maxX = -Infinity;

            children.forEach(child => {
                const result = buildLayout(child, level + 1, currentX);
                minX = Math.min(minX, result.minX);
                maxX = Math.max(maxX, result.maxX);
                currentX += result.width;
            });

            // Place current node
            // Find group of closest children (gap no more than 450px between neighbors)
            const childrenPositions = [];
            children.forEach(child => {
                const childPos = this.nodePositions.get(child.uuid);
                if (childPos) {
                    childrenPositions.push(childPos.x);
                }
            });
            childrenPositions.sort((a, b) => a - b);

            // Find largest group of close children
            let bestGroupStart = 0;
            let bestGroupEnd = 0;
            let currentGroupStart = 0;

            for (let i = 1; i < childrenPositions.length; i++) {
                const gap = childrenPositions[i] - childrenPositions[i - 1];

                if (gap > 450) {
                    // Large gap - start new group
                    const currentGroupSize = i - currentGroupStart;
                    const bestGroupSize = bestGroupEnd - bestGroupStart;

                    if (currentGroupSize > bestGroupSize) {
                        bestGroupStart = currentGroupStart;
                        bestGroupEnd = i;
                    }

                    currentGroupStart = i;
                }
            }

            // Check last group
            const lastGroupSize = childrenPositions.length - currentGroupStart;
            const bestGroupSize = bestGroupEnd - bestGroupStart;
            if (lastGroupSize > bestGroupSize) {
                bestGroupStart = currentGroupStart;
                bestGroupEnd = childrenPositions.length;
            }

            // Center between children from best group
            const groupMinX = childrenPositions[bestGroupStart];
            const groupMaxX = childrenPositions[bestGroupEnd - 1];
            const x = (groupMinX + groupMaxX) / 2;

            this.nodePositions.set(node.uuid, {
                x,
                y,
                node,
                radius: 20
            });

            return { width: maxX - minX + nodeSize, minX, maxX };
        };

        buildLayout(rootNode, 0, 0);

        console.log(`${LOG_PREFIX} Layout calculated: ${this.nodePositions.size} positions`);
    }

    // Check if node is in the same request chain as parent
    isSameRequestChain(node, parentNode) {
        if (!parentNode) return false;

        const parentRequestId = parentNode.metadata?.request_id;
        const childRequestId = node.metadata?.request_id;
        const parentTurnId = parentNode.metadata?.turn_exchange_id;
        const childTurnId = node.metadata?.turn_exchange_id;
        const childMessageType = node.metadata?.message_type;
        return (parentNode.role === 'user' && node.role !== 'user') ||
               (childMessageType === "next" || childMessageType === "variant") ||
               (parentRequestId && parentRequestId === childRequestId) ||
               (parentTurnId && parentTurnId === childTurnId);
    }
    // Get edge style based on node and parent relationship
    getEdgeStyle(node, parentNode, isMainBranch) {
        const isSameChain = this.isSameRequestChain(node, parentNode);
        if (isSameChain) {
            return {
                type: 'double',
                lineWidth: isMainBranch ? 3 : 2,
                dash: [8, 4],
                color1: '#fbbf24', // Yellow
                color2: isMainBranch ? '#3b82f6' : '#475569', // Blue/gray
                dashOffset: 6
            };
        } else {
            return {
                type: 'single',
                lineWidth: isMainBranch ? 3 : 2,
                color: isMainBranch ? '#3b82f6' : '#475569'
            };
        }
    }
    // Draw a bezier curve between two points
    drawBezierCurve(fromPos, toPos) {
        this.ctx.moveTo(fromPos.x, fromPos.y);
        const controlPointY = (fromPos.y + toPos.y) / 2;
        this.ctx.bezierCurveTo(
            fromPos.x, controlPointY,
            toPos.x, controlPointY,
            toPos.x, toPos.y
        );
    }
    // Draw a double-colored dashed edge (yellow + blue/gray)
    drawDoubleEdge(parentPos, nodePos, style) {
        // First layer: Yellow
        this.ctx.beginPath();
        this.ctx.strokeStyle = style.color1;
        this.ctx.lineWidth = style.lineWidth;
        this.ctx.setLineDash(style.dash);
        this.drawBezierCurve(parentPos, nodePos);
        this.ctx.stroke();
        // Second layer: Blue/gray with offset
        this.ctx.beginPath();
        this.ctx.strokeStyle = style.color2;
        this.ctx.lineWidth = style.lineWidth - 1;
        this.ctx.lineDashOffset = style.dashOffset;
        this.drawBezierCurve(parentPos, nodePos);
        this.ctx.stroke();
        // Reset
        this.ctx.setLineDash([]);
        this.ctx.lineDashOffset = 0;
    }
    // Draw a regular solid edge
    drawRegularEdge(parentPos, nodePos, style) {
        this.ctx.beginPath();
        this.ctx.strokeStyle = style.color;
        this.ctx.lineWidth = style.lineWidth;
        this.drawBezierCurve(parentPos, nodePos);
        this.ctx.stroke();
    }
    // Draw a single edge between node and its parent
    drawSingleEdge(node, nodePos, parentPos, parentNode, isMainBranch) {
        const style = this.getEdgeStyle(node, parentNode, isMainBranch);
        if (style.type === 'double') {
            this.drawDoubleEdge(parentPos, nodePos, style);
        } else {
            this.drawRegularEdge(parentPos, nodePos, style);
        }
    }

    drawEdges() {
        const { nodes } = this.treeData;

        nodes.forEach(node => {
            if (!node.parentUuid) return;

            const nodePos = this.nodePositions.get(node.uuid);
            const parentPos = this.nodePositions.get(node.parentUuid);
            if (!nodePos || !parentPos) return;

            const parentNode = this.treeData.nodeMap.get(node.parentUuid);
            if (!parentNode) return;

            const branchInfo = this.treeData.messageToBranch.get(node.uuid);
            const isMainBranch = branchInfo?.isMainBranch || false;

            this.drawSingleEdge(node, nodePos, parentPos, parentNode, isMainBranch);
        });
    }

    drawNodes() {
        this.nodePositions.forEach((pos, uuid) => {
            const node = pos.node;
            const branchInfo = this.treeData.messageToBranch.get(uuid);
            const isMainBranch = branchInfo?.isMainBranch || false;
            const isInActivePath = this.treeData.activePath.has(uuid);
            const isSelected = uuid === this.currentHighlightedNode;

            // Draw circle
            this.ctx.beginPath();
            this.ctx.arc(pos.x, pos.y, pos.radius, 0, Math.PI * 2);
            this.ctx.fillStyle = this.getNodeColor(node);
            this.ctx.fill();

            // Border
            this.ctx.lineWidth = isSelected ? 4 : (isInActivePath ? 3 : 2);
            if (isSelected) {
                // Purple shimmer style
                const time = Date.now() / 1000;
                const pulse = Math.sin(time * 3) * 0.3 + 0.7; // Pulse from 0.4 to 1.0
                // Create gradient for shimmer effect
                const gradient = this.ctx.createRadialGradient(
                    pos.x, pos.y, pos.radius * 0.5,
                    pos.x, pos.y, pos.radius * 2
                );
                // Purple tones with pulse
                gradient.addColorStop(0, `rgba(168, 85, 247, ${pulse})`);   // Bright purple center
                gradient.addColorStop(0.5, `rgba(139, 92, 246, ${pulse * 0.8})`);
                gradient.addColorStop(1, `rgba(109, 40, 217, 0)`); // Radial fade to transparent
                // Glow effect
                this.ctx.shadowColor = '#a855f7'; // Purple shadow color
                this.ctx.shadowBlur = 15 + 5 * Math.sin(time * 5); // Breathing glow
                this.ctx.strokeStyle = '#c084fc'; // Bright purple border
                this.ctx.stroke();
                // Add outer light effect
                this.ctx.save();
                this.ctx.beginPath();
                this.ctx.arc(pos.x, pos.y, pos.radius, 0, Math.PI * 2);
                this.ctx.strokeStyle = gradient;
                this.ctx.lineWidth = 6; // Thicker for glow effect
                this.ctx.stroke();
                this.ctx.restore();
                this.ctx.shadowBlur = 0; // Reset shadow
            } else if (isInActivePath) {
                this.ctx.strokeStyle = '#fbbf24'; // Yellow for main branch
                this.ctx.stroke();
                if (isMainBranch) {
                    this.ctx.shadowColor = '#fbbf24';
                    this.ctx.shadowBlur = 4;
                    this.ctx.stroke();
                    this.ctx.shadowBlur = 0;
                }
            } else {
                // === REGULAR WHITE STYLE ===
                this.ctx.strokeStyle = '#ffffff';
                this.ctx.stroke();
            }

            // Draw nodeIndex number inside the circle
            this.ctx.fillStyle = '#ffffff';
            this.ctx.font = 'bold 11px sans-serif';
            this.ctx.textAlign = 'center';
            this.ctx.textBaseline = 'middle';
            this.ctx.fillText(node.nodeIndex.toString(), pos.x, pos.y);

            // Content indicator (top-right corner)
            const hasContent = node.content && node.content.trim().length > 0;
            const contentColor = hasContent ? '#10b981' : '#64748b'; // Green if exists, else grey
            this.ctx.fillStyle = contentColor;
            this.ctx.beginPath();
            this.ctx.arc(pos.x + 17, pos.y - 17, 5, 0, Math.PI * 2);
            this.ctx.fill();

            // Image indicator (top-left corner)
            const nodeHasImage = this.hasImage(node);
            if (nodeHasImage) {
                this.ctx.fillStyle = '#14b8a6'; // Teal color like in screenshot
                this.ctx.beginPath();
                this.ctx.arc(pos.x - 17, pos.y - 17, 6, 0, Math.PI * 2);
                this.ctx.fill();
                this.ctx.fillStyle = '#ffffff';
                this.ctx.fillRect(pos.x - 17 - 3, pos.y - 17 - 3, 6, 5);
                this.ctx.fillStyle = '#14b8a6';
                this.ctx.beginPath();
                this.ctx.arc(pos.x - 17 + 1, pos.y - 17 - 1, 1.5, 0, Math.PI * 2);
                this.ctx.fill();
            }

            // Draw label
            this.ctx.fillStyle = '#e2e8f0';
            this.ctx.font = isMainBranch ? 'bold 11px sans-serif' : '11px sans-serif';
            this.ctx.textAlign = 'center';
            this.ctx.textBaseline = 'alphabetic'; // Reset baseline
            this.ctx.fillText(this.getNodeLabel(node), pos.x, pos.y + pos.radius + 15);

            // Get version info (sibling index)
            const parent = node.parent;
            let versionIndex = 1;
            let totalVersions = 1;
            if (parent && parent.children.length > 1) {
                const sameRoleSiblings = parent.children
                    .map(uuid => this.treeData.nodeMap.get(uuid))
                    .filter(s => s && s.role === node.role);
                if (sameRoleSiblings.length > 1 &&
                    !(parent.role === 'user' && node.role === 'user')) {
                    versionIndex = sameRoleSiblings.findIndex(s => s.uuid === node.uuid) + 1;
                    totalVersions = sameRoleSiblings.length;
                }
            }

            // Show Version and Branch info
            const branchNumber = branchInfo ? branchInfo.branchIndex : 1;

            this.ctx.fillStyle = '#94a3b8';
            this.ctx.font = '9px sans-serif';

            // Version (if siblings exist)
            if (totalVersions > 1) {
                this.ctx.fillText(`v${versionIndex}/${totalVersions}`, pos.x, pos.y + pos.radius + 28);
            }

            // Branch number
            this.ctx.fillText(`b${branchNumber}`, pos.x, pos.y + pos.radius + (totalVersions > 1 ? 40 : 28));
        });
    }

    findNodeAtPosition(x, y) {
        for (const [uuid, pos] of this.nodePositions) {
            const dx = x - pos.x;
            const dy = y - pos.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance <= pos.radius) {
                return pos.node;
            }
        }
        return null;
    }

    getNodeColor(node) {
        // Check for stop reason - red if interrupted
        const finishDetails = node.metadata?.finish_details;
        const stopReason = finishDetails?.reason || finishDetails?.type;
        if (stopReason && (stopReason === 'client_stopped' || stopReason === 'interrupted' || stopReason === 'user_canceled')) {
            return '#ef4444'; // Red
        }

        const colors = {
            'user': '#3b82f6',      // Blue
            'assistant': '#10b981', // Green
            'system': '#6366f1',    // Indigo
            'tool': '#f59e0b',      // Amber
            'root': '#64748b'       // Gray
        };
        return colors[node.role] || '#64748b';
    }

    getNodeLabel(node) {
        if (node.role === 'user') return 'User';
        if (node.role === 'assistant') return 'GPT';
        if (node.role === 'root') return 'Root';
        return node.role;
    }

    hasImage(node) {
        // Check if node has image in chainNodes
        if (node.chainNodes) {
            return node.chainNodes.some(n => this.hasImageInContent(n));
        }
        // Check if node itself has image
        return this.hasImageInContent(node);
    }
    hasImageInContent(node) {
        if (!node.rawData || !node.rawData.message || !node.rawData.message.content) {
            return false;
        }
        const content = node.rawData.message.content;
        if (content.content_type === 'multimodal_text' && Array.isArray(content.parts)) {
            return content.parts.some(part =>
                part.content_type === 'image_asset_pointer' && part.asset_pointer
            );
        }
        return false;
    }

    highlightNode(uuid) {
        this.currentHighlightedNode = uuid;
        this.renderTree();
    }

    onNodeClick(node) {
        console.log(`${LOG_PREFIX} Node clicked:`, node);
        this.highlightNode(node.uuid);
        this.showNodeDetails(node);
    }

    showNodeDetails(node) {
        const content = node.content || '';
        const timeStr = node.createTime ? new Date(node.createTime * 1000).toLocaleString() : 'N/A';
        const preview = content.substring(0, 100);
        const hasMore = content.length > 100;

        // Get branch info
        const branchInfo = this.treeData.messageToBranch.get(node.uuid);
        const branchNumber = branchInfo ? branchInfo.branchIndex : 1; // Branch number
        const branchStatus = branchInfo?.isMainBranch ? 'Main' : 'Side';

        // Get version info (sibling index)
        const parent = node.parent;
        let versionIndex = 1;
        let totalVersions = 1;
        if (parent && parent.children.length > 1) {
            const sameRoleSiblings = parent.children
                .map(uuid => this.treeData.nodeMap.get(uuid))
                .filter(s => s && s.role === node.role);
            if (sameRoleSiblings.length > 1 &&
                !(parent.role === 'user' && node.role === 'user')) {
                versionIndex = sameRoleSiblings.findIndex(s => s.uuid === node.uuid) + 1;
                totalVersions = sameRoleSiblings.length;
            }
        }

        // Get stop reason from metadata
        const finishDetails = node.metadata?.finish_details;
        const stopReason = finishDetails?.reason || finishDetails?.type || null;

        // Get model from metadata
        const modelSlug = node.metadata?.model_slug || null;

        // Get raw JSON from original mapping
        const rawNodeData = this.treeData.rawMapping ? this.treeData.rawMapping[node.uuid] : null;
        let rawJSON = rawNodeData ? JSON.stringify(rawNodeData, null, 2) : 'N/A';

        // Build displays for merged nodes
        let uuidDisplay = node.uuid;
        let parentDisplay = node.parentUuid || 'ROOT';
        let roleDisplay = node.role;
        let createdDisplay = timeStr;
        let childrenDisplay = node.children.length.toString();
        let unmergedDisplay = '';

        const mergeEnabled = Settings.get('graph', 'mergeAssistantChains');

        if (node.chainNodes && node.chainNodes.length > 0) {
            // Merged node - show all data with nodeIndex
            uuidDisplay = node.chainNodes.map(n => `${n.nodeIndex}: ${n.uuid}`).join('<br>');
            parentDisplay = node.chainNodes.map(n => `${n.nodeIndex}: ${n.parentUuid || 'ROOT'}`).join('<br>');
            roleDisplay = node.chainNodes.map(n => `${n.nodeIndex}: ${n.role}`).join('<br>');
            createdDisplay = node.chainNodes.map(n => {
                const t = n.createTime ? `${new Date(n.createTime * 1000).toLocaleString()} (${n.createTime})` : 'N/A';
                return `${n.nodeIndex}: ${t}`;
            }).join('<br>');
            childrenDisplay = node.chainNodes.map(n => `${n.nodeIndex}: ${n.children.length}`).join('<br>');
            rawJSON = node.chainNodes.map(n => JSON.stringify(n.rawData, null, 2)).join('\n');

            // Unmerged display
            if (mergeEnabled) {
                unmergedDisplay = node.chainNodes.map(n => n.nodeIndex).join(',');
            }
        } else if (mergeEnabled && node.unmergedIndex !== undefined) {
            // Merged mode but NOT chainNode (e.g. User)
            unmergedDisplay = node.unmergedIndex.toString();
        }

        // Create modal
        const modal = document.createElement('div');
        modal.className = 'cpm-graph-modal-overlay';
        modal.innerHTML = `
            <div class="cpm-graph-modal">
                <div class="cpm-graph-modal-header">
                    <h3>${node.nodeIndex.toString()} — ${this.getNodeLabel(node)}</h3>
                    <button class="cpm-graph-modal-close">×</button>
                </div>
                <div class="cpm-graph-modal-body">
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">UUID:</span>
                        <span class="cpm-graph-value">${uuidDisplay}</span>
                    </div>
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Parent UUID:</span>
                        <span class="cpm-graph-value">${node.chainNodes ? parentDisplay : (node.parentUuid || 'ROOT')}</span>
                    </div>
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Role:</span>
                        <span class="cpm-graph-value">${node.chainNodes ? roleDisplay : node.role}</span>
                    </div>
                    ${modelSlug ? `
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Model:</span>
                        <span class="cpm-graph-value">${modelSlug}</span>
                    </div>
                    ` : ''}
                    ${totalVersions > 1 ? `
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Version:</span>
                        <span class="cpm-graph-value">${versionIndex} / ${totalVersions}</span>
                    </div>
                    ` : ''}
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Branch:</span>
                        <span class="cpm-graph-value">${branchNumber}</span>
                    </div>
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Status:</span>
                        <span class="cpm-graph-value cpm-graph-status-${branchInfo?.isMainBranch ? 'main' : 'side'}">${branchStatus}</span>
                    </div>
                    ${stopReason ? `
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Stop Reason:</span>
                        <span class="cpm-graph-value">${stopReason}</span>
                    </div>
                    ` : ''}
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Created:</span>
                        <span class="cpm-graph-value">${node.chainNodes ? createdDisplay : `${timeStr} (${node.createTime})`}</span>
                    </div>
                    ${node.children.length >= 1 ? `
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Children:</span>
                        <span class="cpm-graph-value">${node.chainNodes ? childrenDisplay : node.children}</span>
                    </div>
                    ` : ''}
                    ${unmergedDisplay ? `
                    <div class="cpm-graph-info-row">
                        <span class="cpm-graph-label">Index:</span>
                        <span class="cpm-graph-value">${unmergedDisplay}</span>
                    </div>
                    ` : ''}
                </div>
                ${content ? `
                <div class="cpm-graph-content-section">
                    <div class="cpm-graph-content-header">Content</div>
                    <div class="cpm-graph-content-box">
                        <div class="cpm-graph-content-text" id="content-preview">${escapeHtml(preview)}${hasMore ? '...' : ''}</div>
                        <div class="cpm-graph-content-text" id="content-full" style="display: none;">${escapeHtml(content)}</div>
                    </div>
                    ${hasMore ? '<button class="cpm-graph-content-toggle" id="content-toggle">Show full text</button>' : ''}
                </div>
                ` : ''}
                <div class="cpm-graph-content-section">
                    <div class="cpm-graph-content-header">Raw JSON</div>
                    <div class="cpm-graph-content-box" style="display: none;" id="raw-json-box">
                        <pre class="cpm-graph-content-text">${escapeHtml(rawJSON)}</pre>
                    </div>
                    <button class="cpm-graph-content-toggle" id="json-toggle">Show Raw JSON</button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        // Toggle full text
        const toggleBtn = modal.querySelector('#content-toggle');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', () => {
                const preview = modal.querySelector('#content-preview');
                const full = modal.querySelector('#content-full');

                if (full.style.display === 'none') {
                    preview.style.display = 'none';
                    full.style.display = 'block';
                    toggleBtn.textContent = 'Show less';
                } else {
                    preview.style.display = 'block';
                    full.style.display = 'none';
                    toggleBtn.textContent = 'Show full text';
                }
            });
        }

        // Toggle raw JSON
        const jsonToggleBtn = modal.querySelector('#json-toggle');
        const jsonBox = modal.querySelector('#raw-json-box');
        if (jsonToggleBtn && jsonBox) {
            jsonToggleBtn.addEventListener('click', () => {
                if (jsonBox.style.display === 'none') {
                    jsonBox.style.display = 'block';
                    jsonToggleBtn.textContent = 'Hide Raw JSON';
                } else {
                    jsonBox.style.display = 'none';
                    jsonToggleBtn.textContent = 'Show Raw JSON';
                }
            });
        }

        // Close modal
        const closeModal = () => modal.remove();
        modal.addEventListener('click', (e) => {
            if (e.target === modal) closeModal();
        });

        const closeBtn = modal.querySelector('.cpm-graph-modal-close');
        if (closeBtn) {
            closeBtn.addEventListener('click', closeModal);
        }
    }

    toggle() {
        this.isOpen = !this.isOpen;

        if (this.isOpen) {
            this.open();
        } else {
            this.close();
        }
    }

    async open() {
        console.log(`${LOG_PREFIX} Opening tree panel`);
        this.container.style.display = 'block';
        this.container.style.width = `${this.panelWidth}px`;
        this.isOpen = true;

        const panelMode = Settings.get('graph', 'panelMode');

        if (panelMode === 'push') {
            const main = document.querySelector(SELECTORS.chat.mainContainer);
            if (main) {
                main.style.width = `calc(100% - ${this.panelWidth}px)`;
            }
        }

        // Load tree if not loaded
        if (!this.treeData) {
            await this.loadConversationTree();
        }
    }

    close() {
        console.log(`${LOG_PREFIX} Closing tree panel`);
        this.container.style.display = 'none';
        this.isOpen = false;

        const main = document.querySelector(SELECTORS.chat.mainContainer);
        if (main) {
            main.style.width = '100%';
        }
    }

    async refresh() {
        console.log(`${LOG_PREFIX} Refreshing tree`);

        // Clear current tree data
        this.treeData = null;
        this.nodePositions.clear();

        // Clear canvas
        if (this.ctx && this.canvas) {
            const content = this.container.querySelector('.tree-panel-content');
            const rect = content.getBoundingClientRect();
            const width = rect.width;
            const height = rect.height;

            this.ctx.fillStyle = '#0f172a';
            this.ctx.fillRect(0, 0, width, height);

            this.ctx.fillStyle = '#94a3b8';
            this.ctx.font = '14px sans-serif';
            this.ctx.textAlign = 'center';
            this.ctx.fillText('Refreshing...', width / 2, height / 2);
        }

        await this.loadConversationTree();
    }

    openSettings() {
        console.log(`${LOG_PREFIX} Opening settings`);
        this.createSettingsModal();
    }

    createSettingsModal() {
        // Remove existing modal
        const existing = document.querySelector('.cpm-graph-modal');
        if (existing) {
            existing.remove();
        }

        const modal = document.createElement('div');
        modal.className = 'cpm-graph-modal-overlay';
        modal.innerHTML = `
            <div class="cpm-graph-modal">
                <div class="cpm-graph-modal-header">
                    <h3>Tree Settings</h3>
                    <button class="cpm-graph-modal-close">×</button>
                </div>
                <div class="cpm-graph-modal-body">
                    <div class="cpm-graph-settings-section">
                        <h4>Panel Mode</h4>
                        <select id="panel-mode">
                            <option value="overlay">Overlay</option>
                            <option value="push">Push Content</option>
                        </select>
                    </div>
                    <div class="cpm-graph-settings-section">
                        <h4>Assistant Chains</h4>
                        <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
                            <input type="checkbox" id="merge-assistant-chains" style="width: 16px; height: 16px; cursor: pointer;">
                            <span>Merge system/tool/assistant nodes into one</span>
                        </label>
                    </div>
                </div>
                <div class="cpm-graph-modal-footer">
                    <button class="cpm-graph-btn cpm-graph-btn-primary">Save & Apply</button>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        // Load current settings
        document.getElementById('panel-mode').value = Settings.get('graph', 'panelMode');
        document.getElementById('merge-assistant-chains').checked = Settings.get('graph', 'mergeAssistantChains');

        // Event listeners
        modal.querySelector('.cpm-graph-modal-close').addEventListener('click', () => modal.remove());
        modal.querySelector('.cpm-graph-btn-primary').addEventListener('click', () => {
            this.saveSettings();
            modal.remove();
        });

        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                modal.remove();
            }
        });
    }

    saveSettings() {
        Settings.set('graph', 'panelMode', document.getElementById('panel-mode').value);
        Settings.set('graph', 'mergeAssistantChains', document.getElementById('merge-assistant-chains').checked);
        Settings.save();

        console.log(`${LOG_PREFIX} Settings saved`);
        this.loadConversationTree();
    }

    startResizing(e) {
        this.isResizing = true;
        document.documentElement.style.userSelect = 'none';
        document.addEventListener('mousemove', (e) => this.resizePanel(e));
        document.addEventListener('mouseup', () => this.stopResizing());

        const resizer = this.container.querySelector('.tree-resizer-group');
        if (resizer) resizer.classList.add('resizing');

        e.preventDefault();
    }

    resizePanel(e) {
        if (!this.isResizing) return;

        const newWidth = window.innerWidth - e.clientX;
        if (newWidth >= 300 && newWidth <= window.innerWidth - 100) {
            this.panelWidth = newWidth;
            this.container.style.width = `${newWidth}px`;

            // Update push mode
            const mode = Settings.get('graph', 'panelMode');
            if (mode === 'push' && this.isOpen) {
                const main = document.querySelector(SELECTORS.chat.mainContainer);
                if (main) {
                    main.style.width = `calc(100% - ${newWidth}px)`;
                }
            }

            // Re-render with new size
            setTimeout(() => {
                if (this.treeData) {
                    this.renderTree();
                }
            }, 10);
        }
    }

    stopResizing() {
        this.isResizing = false;
        document.documentElement.style.userSelect = '';
        document.removeEventListener('mousemove', (e) => this.resizePanel(e));
        document.removeEventListener('mouseup', () => this.stopResizing());

        const resizer = this.container.querySelector('.tree-resizer-group');
        if (resizer) resizer.classList.remove('resizing');
    }
}

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

let treePanel = null;

async function initialize() {
    console.log(`${LOG_PREFIX} Initializing ChatGPT Conversation Tree`);

    // Wait for page to load
    await new Promise(resolve => setTimeout(resolve, CONFIG.initialDelay));

    // Check if we're in a conversation
    const conversationId = getConversationId();
    if (!conversationId) {
        console.log(`${LOG_PREFIX} Not in a conversation, waiting...`);
        // Watch for navigation
        watchForConversation();
        return;
    }

    // Initialize tree panel
    treePanel = new ConversationTreePanel();
    await treePanel.initialize();

    console.log(`${LOG_PREFIX} Initialization complete`);
}

function watchForConversation() {
    // Watch URL changes
    let lastUrl = window.location.href;

    const observer = new MutationObserver(() => {
        const currentUrl = window.location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            const conversationId = getConversationId();

            if (conversationId && !treePanel) {
                console.log(`${LOG_PREFIX} Conversation detected, initializing...`);
                initialize();
            } else if (!conversationId && treePanel) {
                console.log(`${LOG_PREFIX} Left conversation`);
                if (treePanel.isOpen) {
                    treePanel.close();
                }
            } else if (conversationId && treePanel) {
                console.log(`${LOG_PREFIX} Conversation changed, refreshing...`);
                treePanel.refresh();
            }
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
}

// =============================================
// STYLES
// =============================================

GM_addStyle(`
    /* Resizer */
    .tree-resizer-group {
        position: absolute;
        top: 50%;
        left: -7px;
        width: 14px;
        height: 100%;
        transform: translateY(-50%);
        cursor: col-resize;
        z-index: 10;
        user-select: none;
        display: grid;
        place-items: center;
        transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .tree-resizer-line {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 50%;
        width: 0.5px;
        background: rgba(148, 163, 184, 0.2);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        transition-delay: 50ms;
        transform: translateX(-50%);
        transform-origin: center;
    }

    .tree-resizer-handle {
        position: relative;
        width: 8px;
        height: 24px;
        border-radius: 9999px;
        border: 0.5px solid rgba(148, 163, 184, 0.3);
        background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
        box-shadow:
            0 1px 3px rgba(0, 0, 0, 0.2),
            inset 0 1px 0 rgba(255, 255, 255, 0.05);
        cursor: col-resize;
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        z-index: 1;
        overflow: hidden;
    }

    .tree-resizer-handle::before {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        width: 2px;
        height: 8px;
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(148, 163, 184, 0.15) 20%,
            rgba(148, 163, 184, 0.25) 50%,
            rgba(148, 163, 184, 0.15) 80%,
            transparent
        );
        border-radius: 1px;
        transform: translate(-50%, -50%);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .tree-resizer-group:hover .tree-resizer-line {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(102, 126, 234, 0.4) 20%,
            rgba(102, 126, 234, 0.7) 50%,
            rgba(102, 126, 234, 0.4) 80%,
            transparent
        );
        width: 2px;
        box-shadow:
            0 0 8px rgba(102, 126, 234, 0.3),
            0 0 16px rgba(102, 126, 234, 0.1);
    }

    .tree-resizer-group:hover .tree-resizer-handle {
        border-color: rgba(102, 126, 234, 0.6);
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        box-shadow:
            0 0 0 2px rgba(102, 126, 234, 0.2),
            0 2px 8px rgba(0, 0, 0, 0.3),
            0 0 12px rgba(102, 126, 234, 0.4),
            inset 0 1px 0 rgba(255, 255, 255, 0.2);
        transform: scale(1.05);
    }

    .tree-resizer-group:hover .tree-resizer-handle::before {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(255, 255, 255, 0.2) 20%,
            rgba(255, 255, 255, 0.4) 50%,
            rgba(255, 255, 255, 0.2) 80%,
            transparent
        );
        box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
    }

    .tree-resizer-group:active .tree-resizer-handle,
    .tree-resizer-group.resizing .tree-resizer-handle {
        border-color: rgba(118, 75, 162, 0.8);
        background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
        box-shadow:
            0 0 0 3px rgba(118, 75, 162, 0.4),
            0 4px 12px rgba(0, 0, 0, 0.4),
            0 0 20px rgba(118, 75, 162, 0.6),
            inset 0 1px 0 rgba(255, 255, 255, 0.3);
        transform: scale(1.1);
    }

    .tree-resizer-group:active .tree-resizer-line,
    .tree-resizer-group.resizing .tree-resizer-line {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(118, 75, 162, 0.6) 10%,
            rgba(118, 75, 162, 0.9) 50%,
            rgba(118, 75, 162, 0.6) 90%,
            transparent
        );
        width: 3px;
        box-shadow:
            0 0 12px rgba(118, 75, 162, 0.5),
            0 0 24px rgba(118, 75, 162, 0.3);
    }

    /* Toggle Button */
    .tree-toggle-btn {
        position: fixed;
        bottom: 20px;
        right: 20px;
        width: 50px;
        height: 50px;
        border-radius: 50%;
        background: linear-gradient(135deg, rgba(14, 20, 30, 0.95) 0%, rgba(10, 17, 32, 0.9) 100%);
        border: 1.5px solid rgba(59, 130, 246, 0.4);
        color: white;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        backdrop-filter: blur(10px);
        transition: all 0.3s ease;
        z-index: 9999;
    }

    .tree-toggle-btn:hover {
        transform: scale(1.05);
        border-color: rgba(59, 130, 246, 0.7);
        box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.4),
                    0 0 40px rgba(59, 130, 246, 0.6),
                    0 0 60px rgba(59, 130, 246, 0.3);
    }

    .tree-toggle-btn svg {
        filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
    }

    /* Panel Wrapper */
    .tree-panel-wrapper {
        position: fixed;
        top: 0;
        right: 0;
        width: 400px;
        height: 100vh;
        background: rgba(15, 23, 42, 0.95);
        backdrop-filter: blur(10px);
        border-left: 1px solid rgba(71, 85, 105, 0.3);
        z-index: 9998;
        box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
    }

    /* Panel */
    .tree-panel {
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: column;
        color: #e2e8f0;
    }

    /* Header */
    .tree-panel-header {
        padding: 16px 20px;
        border-bottom: 1px solid rgba(71, 85, 105, 0.3);
        background: rgba(30, 41, 59, 0.6);
    }

    .tree-header-content {
        display: flex;
        align-items: center;
        justify-content: space-between;
    }

    .tree-header-content h3 {
        margin: 0;
        font-size: 16px;
        font-weight: 600;
        color: #f1f5f9;
    }

    .tree-header-controls {
        display: flex;
        gap: 8px;
    }

    .tree-control-btn {
        width: 32px;
        height: 32px;
        border-radius: 6px;
        background: rgba(51, 65, 85, 0.5);
        border: 1px solid rgba(71, 85, 105, 0.3);
        color: #cbd5e1;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: all 0.2s;
    }

    .tree-control-btn:hover {
        background: rgba(71, 85, 105, 0.6);
        border-color: rgba(100, 116, 139, 0.5);
        color: #f1f5f9;
    }

    /* Content */
    .tree-panel-content {
        flex: 1;
        overflow: hidden;
        position: relative;
    }

    .tree-loading, .tree-error {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100%;
        color: #94a3b8;
        font-size: 14px;
    }

    .tree-error {
        color: #ef4444;
    }

    .tree-canvas {
        width: 100%;
        height: 100%;
        cursor: grab;
    }

    .tree-canvas:active {
        cursor: grabbing;
    }

    /* Modal Overlay */
    .cpm-graph-modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.6);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 10001;
        animation: fadeIn 0.2s ease;
    }

    @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
    }

    /* Modal Window */
    .cpm-graph-modal {
        background: linear-gradient(to bottom, #1e293b 0%, #0f172a 100%);
        border-radius: 16px;
        box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(59, 130, 246, 0.2);
        min-width: 420px;
        max-width: 520px;
        max-height: 85vh;
        animation: slideUp 0.3s ease;
        overflow: hidden;
        display: flex;
        flex-direction: column;
    }

    @keyframes slideUp {
        from {
            transform: translateY(20px);
            opacity: 0;
        }
        to {
            transform: translateY(0);
            opacity: 1;
        }
    }

    /* Modal Header */
    .cpm-graph-modal-header {
        padding: 24px;
        background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
        border-bottom: 1px solid rgba(59, 130, 246, 0.2);
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .cpm-graph-modal-header h3 {
        margin: 0;
        color: #e2e8f0;
        font-size: 20px;
        font-weight: 700;
        background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
    }

    .cpm-graph-modal-close {
        background: rgba(51, 65, 85, 0.5);
        border: none;
        color: #94a3b8;
        font-size: 24px;
        line-height: 1;
        cursor: pointer;
        padding: 0;
        width: 36px;
        height: 36px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 8px;
        transition: all 0.2s;
        font-weight: 300;
    }

    .cpm-graph-modal-close:hover {
        background: rgba(239, 68, 68, 0.2);
        color: #f87171;
        transform: rotate(90deg);
    }

    /* Modal Body */
    .cpm-graph-modal-body {
        padding: 24px;
        overflow-y: auto;
        flex: 1;
    }

    .cpm-graph-modal-body::-webkit-scrollbar {
        width: 6px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-track {
        background: rgba(51, 65, 85, 0.3);
        border-radius: 3px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.5);
        border-radius: 3px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-thumb:hover {
        background: rgba(148, 163, 184, 0.7);
    }

    .cpm-graph-info-row {
        display: flex;
        margin-bottom: 14px;
        align-items: flex-start;
        padding: 8px 12px;
        background: rgba(51, 65, 85, 0.3);
        border-radius: 6px;
        transition: background 0.2s;
    }

    .cpm-graph-info-row:hover {
        background: rgba(51, 65, 85, 0.5);
        transform: translateX(2px);
    }

    .cpm-graph-info-row:last-child {
        margin-bottom: 0;
    }

    .cpm-graph-label {
        color: #94a3b8;
        font-size: 13px;
        font-weight: 600;
        min-width: 110px;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 11px;
    }

    .cpm-graph-value {
        color: #e2e8f0;
        font-size: 13px;
        font-family: 'Monaco', 'Menlo', monospace;
        word-break: break-all;
        line-height: 1.5;
    }

    /* Content Section */
    .cpm-graph-content-section {
        padding: 16px 24px;
        border-top: 1px solid rgba(51, 65, 85, 0.5);
        background: rgba(15, 23, 42, 0.3);
    }

    .cpm-graph-content-header {
        color: #94a3b8;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        margin-bottom: 10px;
    }

    .cpm-graph-content-box {
        background: rgba(15, 23, 42, 0.6);
        border: 1px solid rgba(71, 85, 105, 0.3);
        border-radius: 8px;
        padding: 14px;
        max-height: 250px;
        overflow-y: auto;
    }

    .cpm-graph-content-box::-webkit-scrollbar {
        width: 6px;
    }

    .cpm-graph-content-box::-webkit-scrollbar-track {
        background: rgba(51, 65, 85, 0.3);
        border-radius: 3px;
    }

    .cpm-graph-content-box::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.5);
        border-radius: 3px;
    }

    .cpm-graph-content-text {
        color: #cbd5e1;
        font-family: 'Roboto Mono', monospace;
        font-size: 11px;
        line-height: 1.6;
        white-space: pre-wrap;
        word-wrap: break-word;
        overflow-wrap: break-word;
    }

    .cpm-graph-content-toggle {
        margin-top: 8px;
        padding: 6px 12px;
        width: 100%;
        background: rgba(59, 130, 246, 0.2);
        border: 1px solid rgba(59, 130, 246, 0.3);
        border-radius: 6px;
        color: #60a5fa;
        font-size: 11px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s;
    }

    .cpm-graph-content-toggle:hover {
        background: rgba(59, 130, 246, 0.3);
        border-color: rgba(59, 130, 246, 0.5);
    }

    .cpm-graph-status-main {
        color: #60a5fa !important;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 12px;
        background: rgba(59, 130, 246, 0.2);
        padding: 2px 8px;
        border-radius: 4px;
        font-family: sans-serif;
        display: inline-block;
    }

    .cpm-graph-status-side {
        color: #94a3b8 !important;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 12px;
        background: rgba(100, 116, 139, 0.2);
        padding: 2px 8px;
        border-radius: 4px;
        font-family: sans-serif;
        display: inline-block;
    }

    .cpm-graph-modal-footer {
        padding: 16px 24px;
        border-top: 1px solid rgba(71, 85, 105, 0.3);
        background: rgba(30, 41, 59, 0.6);
    }

    .cpm-graph-btn.cpm-graph-btn-primary {
        width: 100%;
        padding: 10px 24px;
        background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
        border: none;
        border-radius: 8px;
        color: white;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.2s;
        box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
    }

    .cpm-graph-btn.cpm-graph-btn-primary:hover {
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
    }

    .cpm-graph-btn.cpm-graph-btn-primary:active {
        transform: translateY(0);
    }

    /* Settings Modal */

    .cpm-graph-settings-section {
        margin-bottom: 20px;
    }

    .cpm-graph-settings-section:last-child {
        margin-bottom: 0;
    }

    .cpm-graph-settings-section h4 {
        margin: 0 0 10px 0;
        color: #cbd5e1;
        font-size: 13px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.5px;
    }

    .cpm-graph-settings-section select {
        width: 100%;
        padding: 10px 12px;
        background: rgba(30, 41, 59, 0.6);
        border: 1px solid rgba(71, 85, 105, 0.3);
        border-radius: 8px;
        color: #e2e8f0;
        font-size: 14px;
        cursor: pointer;
        transition: all 0.2s;
    }

    .cpm-graph-settings-section select:hover {
        border-color: rgba(100, 116, 139, 0.5);
        background: rgba(30, 41, 59, 0.8);
    }

    .cpm-graph-settings-section select:focus {
        outline: none;
        border-color: #3b82f6;
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    .cpm-graph-settings-section label {
        display: flex;
        align-items: center;
        gap: 10px;
        color: #cbd5e1;
        font-size: 14px;
        cursor: pointer;
    }

    .cpm-graph-settings-section input[type="checkbox"] {
        width: 18px;
        height: 18px;
        cursor: pointer;
    }

    /* Responsive */
    @media (max-width: 768px) {
        .tree-panel-wrapper {
            width: 100%;
        }

        .tree-toggle-btn {
            bottom: 80px;
        }
    }
`);

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initialize);
} else {
    initialize();
}

})();