ChatGPT Message Info

Add conversation tree visualization with nodes for ChatGPT

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();