在你是一位资深的前端程序员，下面有一段HTML程序源代码（注：使用适合的思维导图外部库，将html/css/js写在同一个文件，并只用写在同一个代码框中，代码不能太简单，要满足下面所有的需求），用于将特定格式的日志进行节点化的可视化展示和基于结构化、思维导图化的精简化处理，对标Xmind。该程序的主要功能是解析并展示以下格式的日志信息，以下是该程序的逻辑说明：

##逻辑说明

部分示例日志：
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiInfoCache::getCustomKpi,retsize=255,netype=-9,solutionVer=3100
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: getKPIFormula use cache
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiMgrComponentImpl::getKPIFormula Exited...
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiMgrComponentImpl::getRawItemFormula Entered...
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiInfoCache::getCustomKpi,retsize=255
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: getRawItemFormula use cache neTypeId:295 versionId:1586450140
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiMgrComponentImpl::getRawItemFormula Exited...
I001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: KpiMgrComponentImpl::getFormulaByVersion_i Exited...
E001:10-26 10:33:23.194(2355912|2854059)Sum.Rpt: mapCounterParseInfo number:1854
日志解析规则
日志等级：日志开头字符表示日志等级，I表示信息日志，E表示错误日志。
时间戳：日志格式中的10-26 10:33:23.192部分表示日志发生的时间。
进程和线程号：(2355912|2400359)分别表示进程号和线程号。
日志内容：日志正文内容在其后的部分。
如果内容以Entered...或Exited...结尾，表示函数的进入或退出;
如果内容并不以上述内容结尾，则这只是一般的日志。

可视化需求
输入方式：日志文件通过
文本区域输入。</p>
<p>日志按线程分类：根据线程号将日志按部分分组。<br>界面分为左右两个部分</p>
<p>左部分：<br>线程关系组件：显示线程间关系结构，每个线程部分用可交互的节点表示，支持收缩和展开操作。<br>当发现一个线程在另一个线程的执行过程中启动，则该线程被视为从该线程生成，并呈现为嵌套关系。</p>
<p>右部分：<br>日志跟踪组件：当用户点击线程关系组件中的某个线程节点时，日志跟踪组件显示对应线程的详细日志解析结果。日志按照函数调用的执行顺序和嵌套层级（Entered...和Exited...），以可逐级收缩展开的层级节点展示。对于一般的日志，直接呈现在同一个函数层级即可。<br>层级节点组织：<br>按日志的时间顺序和线程嵌套关系，将日志整理为层级结构。</p>
<p>可视化组件：<br>节点显示要求：<br>每个节点按日志格式突出显示发生的时间和日志等级，主要显示日志内容。节点不应该只是简单的一行文字，而是像思维导图一样的呈现，节点之间通过优美的线条去连接，而且又能通过节点整体的缩进体现层级关系。<br>支持鼠标右键标记/去标记不重要的节点，使其变为半透明显示，便于用户聚焦关键日志信息。</p>
<p>注意事项<br>线程关系展示：线程关系组件中，重点是展示线程执行层级和嵌套关系，能够伸缩展开，体现线程间的创建关系。<br>代码优化：优化程序性能，确保日志节点的标记和取消标记功能流畅，且能快速高效地更新节点透明度。</p>

## 你要做的事情
你要做的事情是，在尽量不对源程序代码的逻辑进行修改的前提下，增加以下新的功能：
1、当点击标记/去标记重要性右键菜单选项时，还能使下面的子节点切换显示/隐藏状态，且该状态可以伴随日志数据导入导出；
2、为程序添加一个新的右键菜单功能：根据节点生成时序图。通过节点的层级关系来确定各个类函数之间的调用关系，因此，你只需分析内容以Entered...或Exited...结尾的日志，并从日志中提取类的名称和函数的名称（如，KpiMgrComponentImpl::getFormulaByVersion_i的类名是KpiMgrComponentImpl，函数名是getFormulaByVersion_i，并且，如果一个节点被标记为非重要的，不要让该节点及其主节点成为时序图的一部分），还能根据节点的备注，在对应的流程中添加对应的note，并且，时序图还要加入函数返回的部分。将生成的时序图plantuml代码保存到用户剪贴板。

## 程序源代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>日志可视化工具</title>
    <!-- D3.js CDN -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- FileSaver.js 用于导出文件 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
    <!-- html2canvas 用于导出 SVG 为图片 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
    <!-- 基本样式 -->
    <style>
        body, html {
            margin: 0;
            padding: 0;
            height: 100%;
            font-family: Arial, sans-serif;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        header {
            background-color: #2c3e50;
            color: white;
            text-align: center;
            flex: 0 0 auto;
            padding: 5px 0;
        }
        #controls {
            padding: 10px;
            background-color: #ecf0f1;
            overflow: auto;
            display: flex;
            flex-direction: column;
            gap: 10px;
            flex: 0 0 auto;
        }
        #controls .toggle-section {
            display: flex;
            align-items: center;
            gap: 10px;
            flex-wrap: wrap;
        }
        #controls textarea {
            width: 100%;
            height: 100px;
            resize: vertical;
            padding: 10px;
            box-sizing: border-box;
            font-family: monospace;
            font-size: 14px;
            display: none;
        }
        #controls .thread-section,
        #controls .input-section {
            display: flex;
        }
        .thread-section {
            flex: 0 2 auto !important;
        }
        #controls button, #controls input[type="file"] {
            padding: 5px 10px;
            font-size: 14px;
            background-color: #5980b9;
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
            margin-right: 10px;
        }
        #controls button:hover, #controls input[type="file"]:hover {
            background-color: #9498db;
        }
        #main {
            display: flex;
            flex: 1 2 auto;
            overflow: hidden;
        }
        .container {
            flex: 1;
            position: relative;
            border: 1px solid #bdc3c7;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        .container::after {
            content: attr(data-title);
            position: absolute;
            top: 5px;
            left: 10px;
            background: rgba(255,255,255,0.8);
            padding: 2px 5px;
            border-radius: 3px;
            font-weight: bold;
            z-index: 1;
        }
        .tree-container {
            flex: 1;
            width: 100%;
            height: 100%;
        }
        /* 自定义右键菜单样式 */
        .custom-context-menu {
            position: absolute;
            z-index: 1000;
            width: 200px;
            background: white;
            border: 1px solid #bdc3c7;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
            display: none;
        }
        .custom-context-menu ul {
            list-style: none;
            margin: 0;
            padding: 5px 0;
        }
        .custom-context-menu ul li {
            padding: 8px 12px;
            cursor: pointer;
        }
        .custom-context-menu ul li:hover {
            background-color: #ecf0f1;
        }
        /* Tooltip样式 */
        .tooltip {
            position: absolute;
            text-align: left;
            width: auto;
            padding: 10px;
            font: 12px sans-serif;
            background: rgba(255, 255, 255, 0.9);
            border: 1px solid #bdc3c7;
            border-radius: 4px;
            pointer-events: none;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
        }
        /* 备注弹窗样式 */
        .modal {
            display: none; 
            position: fixed; 
            z-index: 2000; 
            left: 0;
            top: 0;
            width: 100%; 
            height: 100%; 
            overflow: auto;
            background-color: rgba(0,0,0,0.4); 
        }
        .modal-content {
            background-color: #fefefe;
            margin: 10% auto; 
            padding: 20px;
            border: 1px solid #888;
            width: 300px; 
            border-radius: 5px;
        }
        .modal-content textarea {
            width: 100%;
            height: 80px;
            resize: vertical;
            padding: 5px;
            box-sizing: border-box;
            font-family: monospace;
            font-size: 14px;
        }
        .color-palette {
            display: flex;
            gap: 5px;
            margin-top: 10px;
            flex-wrap: wrap;
        }
        .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            cursor: pointer;
            border-radius: 3px;
        }
        .color-swatch.selected {
            border: 2px solid #000;
        }
        .modal-buttons {
            margin-top: 10px;
            text-align: right;
        }
        .modal-buttons button {
            margin-left: 5px;
            padding: 5px 10px;
            font-size: 14px;
        }
        /* Collapse/Expand Buttons */
        .collapse-button {
            padding: 5px 10px;
            font-size: 12px;
            background-color: #95a5a6 !important;
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
        }
        .collapse-button:hover {
            background-color: #7f8c8d !important;
        }
        /* 节点样式 */
        .node rect {
            cursor: pointer;
        }
        .node text {
            pointer-events: none;
        }
    </style>
</head>
<body>
    <header>w
       日志可视化工具
    </header>
    <div id="controls">
        <textarea id="log-input" placeholder="在此处粘贴日志内容..."></textarea>
        <div class="toggle-section">
            <button id="parse-button">解析并可视化</button>
            <button id="export-json">导出日志数据</button>
            <input type="file" id="import-json" accept=".json">
            <button class="collapse-button" id="toggle-input">展开输入区域</button>
            <button class="collapse-button" id="toggle-threads">展开线程区域</button>
        </div>
    </div>
    <div id="main">
        <div class="container thread-section" data-title="线程关系" style="display: flex">
            <svg id="thread-tree" class="tree-container"></svg>
        </div>
        <div class="container" data-title="日志跟踪">
            <svg id="log-tree" class="tree-container"></svg>
        </div>
    </div>

    <!-- 自定义右键菜单 -->
    <div id="context-menu" class="custom-context-menu">
        <ul>
            <li id="toggle-importance">标记/去标记重要性</li>
            <li id="toggle-importance">该函数的资料</li>
            <li id="add-remark">添加备注</li>
        </ul>
    </div>

    <!-- Tooltip -->
    <div id="tooltip" class="tooltip" style="opacity:0;"></div>

    <!-- 备注弹窗 -->
    <div id="remark-modal" class="modal">
        <div class="modal-content">
            <h3>添加备注</h3>
            <textarea id="remark-text" placeholder="在此处输入备注..."></textarea>
            <div>
                <label>选择颜色:</label>
                <div class="color-palette" id="color-palette">
                    <div class="color-swatch" data-color="#FFFFFF" style="background-color: #FFFFFF;"></div>
                    <div class="color-swatch" data-color="#FFCDD2" style="background-color: #FFCDD2;"></div>
                    <div class="color-swatch" data-color="#C8E6C9" style="background-color: #C8E6C9;"></div>
                    <div class="color-swatch" data-color="#BBDEFB" style="background-color: #BBDEFB;"></div>
                    <div class="color-swatch" data-color="#FFF9C4" style="background-color: #FFF9C4;"></div>
                    <div class="color-swatch" data-color="#D1C4E9" style="background-color: #D1C4E9;"></div>
                    <div class="color-swatch" data-color="#B0BEC5" style="background-color: #B0BEC5;"></div>
                </div>
            </div>
            <div class="modal-buttons">
                <button id="save-remark">保存</button>
                <button id="cancel-remark">取消</button>
            </div>
        </div>
    </div>

    <script>
        window.onbeforeunload = function(event) {
            event.returnValue = "确定？";
        };
        // 全局变量存储解析后的日志数据
        let parsedLogs = {};
        let threadParents = {};
        let markedNodes = new Set();
        let nodeStyles = {}; // 存储节点的备注和颜色

        let selectedNode = {
            type: null, // 'thread' 或 'log'
            id: null,
            data: null,
            tree: null
        };

        // 右键菜单元素
        const contextMenu = document.getElementById('context-menu');
        const toggleImportanceMenu = document.getElementById('toggle-importance');
        const addRemarkMenu = document.getElementById('add-remark');

        // Tooltip元素
        const tooltip = d3.select("#tooltip");

        // 备注弹窗元素
        const remarkModal = document.getElementById('remark-modal');
        const remarkText = document.getElementById('remark-text');
        const colorPalette = document.getElementById('color-palette');
        const saveRemarkButton = document.getElementById('save-remark');
        const cancelRemarkButton = document.getElementById('cancel-remark');
        let selectedColor = "#FFFFFF";

        // 切换输入区域显示
        const toggleInputButton = document.getElementById('toggle-input');
        const logInput = document.getElementById('log-input');
        toggleInputButton.addEventListener('click', () => {
            if (logInput.style.display === 'none') {
                logInput.style.display = 'block';
                toggleInputButton.textContent = '收起输入区域';
            } else {
                logInput.style.display = 'none';
                toggleInputButton.textContent = '展开输入区域';
            }
        });

        // 切换线程区域显示
        const toggleThreadsButton = document.getElementById('toggle-threads');
        const threadSection = document.querySelector('.thread-section');
        toggleThreadsButton.addEventListener('click', () => {
            if (threadSection.style.display === 'none') {
                threadSection.style.display = 'flex';
                toggleThreadsButton.textContent = '收起线程区域';
            } else {
                threadSection.style.display = 'none';
                toggleThreadsButton.textContent = '展开线程区域';
            }
        });

        // 初始化区域为隐藏
        logInput.style.display = 'none';
        threadSection.style.display = 'none';

        // 事件绑定：解析并可视化日志
        document.getElementById('parse-button').addEventListener('click', function() {
            const logText = document.getElementById('log-input').value;
            if (logText.trim() === "") {
                alert("日志内容不能为空！");
                return;
            }
            parseLogs(logText);
            renderThreadTree();
            clearLogTree();
        });

        /**
         * 解析日志文本
         * @param {string} text - 日志文本
         */
        function parseLogs(text) {
            const lines = text.split('\n').filter(line => line.trim() !== '');
            parsedLogs = {};
            threadParents = {};
            nodeStyles = {};
            markedNodes = new Set();

            const logEntries = [];

            const regex = /^(I|E)\d{3}:(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\((\d+)\|(\d+)\)Sum\.Rpt: (.+)$/;

            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                const match = line.match(regex);
                if (match) {
                    const [_, level, timestamp, processId, threadId, content] = match;
                    const log = { 
                        level, 
                        timestamp, 
                        processId, 
                        threadId, 
                        content, 
                        raw: line,
                        uniqueId: `log-${threadId}-${i}` // 生成唯一ID
                    };
                    logEntries.push(log);
                    if (!parsedLogs[threadId]) {
                        parsedLogs[threadId] = [];
                    }
                    parsedLogs[threadId].push(log);
                }
            }

            // 处理线程关系
            inferThreadRelationships(logEntries);
        }

        /**
         * 推断线程之间的关系
         * 简化假设：一个线程的启动时间在另一个线程的启动时间之后，则视为子线程
         * @param {Array} logEntries - 所有日志条目
         */
        function inferThreadRelationships(logEntries) {
            // 获取每个线程的最早时间
            const threadStartTimes = {};
            for (const threadId in parsedLogs) {
                const firstLog = parsedLogs[threadId][0];
                // 假设日期为当前月份，年份为当前年
                const currentDate = new Date();
                const year = currentDate.getFullYear();
                const [month, day] = firstLog.timestamp.split(' ')[0].split('-');
                const timePart = firstLog.timestamp.split(' ')[1];
                const date = new Date(`${year}-${month}-${day}T${timePart}Z`);
                threadStartTimes[threadId] = date;
            }

            // 排序线程按开始时间
            const sortedThreads = Object.keys(threadStartTimes).sort((a, b) => threadStartTimes[a] - threadStartTimes[b]);

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

            // 使用虚拟根节点
            threadParents['root'] = null; // 虚拟根

            // 第一线程作为根线程的子线程
            for (let i = 0; i < sortedThreads.length; i++) {
                const childThread = sortedThreads[i];
                if (i === 0) {
                    threadParents[childThread] = 'root';
                } else {
                    // 根据启动时间找到最近的一个父线程
                    let parentThread = 'root';
                    for (let j = i - 1; j >= 0; j--) {
                        const potentialParent = sortedThreads[j];
                        if (threadStartTimes[potentialParent] < threadStartTimes[childThread]) {
                            parentThread = potentialParent;
                            break;
                        }
                    }
                    threadParents[childThread] = parentThread;
                }
            }
        }

        /**
         * 渲染线程关系树
         */
        function renderThreadTree() {
            // 清除旧的SVG
            d3.select("#thread-tree").selectAll("*").remove();

            // 构建树形结构数据，包括虚拟根
            const treeData = buildThreadTree('root', threadParents);

            const margin = { top: 20, right: 200, bottom: 20, left: 200 },
                  width = document.getElementById('thread-tree').clientWidth - margin.left - margin.right,
                  height = document.getElementById('thread-tree').clientHeight - margin.top - margin.bottom;

            const svg = d3.select("#thread-tree")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
              .append("g")
                .attr("transform", `translate(${margin.left},${margin.top})`);

            // 创建树布局，使用固定的节点大小
            const treemap = d3.tree()
                .nodeSize([100, 200]); // [垂直间距, 水平间距]

            // 创建层级结构
            const root = d3.hierarchy(treeData, d => d.children);

            // 生成树布局
            treemap(root);

            // Links
            const link = svg.selectAll(".link")
                .data(root.links())
              .enter().append("path")
                .attr("class", "link")
                .attr("d", d3.linkHorizontal()
                    .x(d => d.y)
                    .y(d => d.x))
                .attr("fill", "none")
                .attr("stroke", "#ccc")
                .attr("stroke-width", 2);

            // Nodes
            const node = svg.selectAll(".node")
                .data(root.descendants())
              .enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `translate(${d.y},${d.x})`)
                .on("click", (event, d) => {
                    if (d.data.id !== 'root') { // 排除虚拟根
                        const threadId = d.data.id;
                        currentSelectedThreadId = threadId;
                        renderLogTree(threadId);
                    }
                })
                .on("contextmenu", (event, d) => {
                    event.preventDefault();
                    if (d.data.id !== 'root') { // 排除虚拟根
                        selectedNode = { type: 'thread', id: `thread-${d.data.id}`, data: d.data, tree: 'thread' };
                        showContextMenu(event.pageX, event.pageY);
                    }
                });

            // Node rectangles
            node.append("rect")
                .attr("width", 150)
                .attr("height", 40)
                .attr("x", -75)
                .attr("y", -20)
                .attr("rx", 5)
                .attr("ry", 5)
                .attr("fill", d => d.data.id === 'root' ? "#ffffff00" : getNodeColor(`thread-${d.data.id}`))
                .attr("stroke", "#0288D1")
                .attr("stroke-width", 2)
                .attr("opacity", d => markedNodes.has(`thread-${d.data.id}`) ? 0.5 : 1.0);

            // Node labels
            node.append("text")
                .attr("dy", 5)
                .attr("text-anchor", "middle")
                .attr("fill", d => isDarkColor(getNodeColor(`thread-${d.data.id}`)) ? "#ffffff" : "#000000")
                .style("pointer-events", "none")
                .text(d => d.data.id === 'root' ? '' : `线程 ${d.data.id}`);

            // 备注显示
            node.append("text")
                .attr("dy", 25)
                .attr("text-anchor", "middle")
                .attr("fill", "#555555")
                .style("font-size", "10px")
                .text(d => {
                    if (nodeStyles[`thread-${d.data.id}`] && nodeStyles[`thread-${d.data.id}`].remark) {
                        return nodeStyles[`thread-${d.data.id}`].remark.length > 15
                            ? nodeStyles[`thread-${d.data.id}`].remark.substring(0, 15) + '...'
                            : nodeStyles[`thread-${d.data.id}`].remark;
                    }
                    return '';
                });

            // 为树添加缩放和平移
            const zoom = d3.zoom()
                .scaleExtent([0.5, 2])
                .on("zoom", (event) => {
                    svg.attr("transform", event.transform);
                });

            d3.select("#thread-tree").call(zoom);
        }

        /**
         * 构建线程树结构，包括虚拟根
         * @param {string} rootId - 虚拟根ID
         * @param {Object} parents - 线程父子关系
         * @returns {Object} - 树形结构
         */
        function buildThreadTree(rootId, parents) {
            const node = { id: rootId, children: [] };
            for (const child in parents) {
                if (parents[child] === rootId && child !== 'root') {
                    node.children.push(buildThreadTree(child, parents));
                }
            }
            return node;
        }

        /**
         * 清除日志树
         */
        function clearLogTree() {
            d3.select("#log-tree").selectAll("*").remove();
            const svg = d3.select("#log-tree");
            svg.append("text")
                .attr("x", "50%")
                .attr("y", "50%")
                .attr("text-anchor", "middle")
                .attr("fill", "#7f8c8d")
                .text("请选择左侧的线程以查看日志");
        }

        let currentSelectedThreadId = null;

        /**
         * 渲染指定线程的日志树
         * @param {string} threadId - 线程ID
         */
        function renderLogTree(threadId) {
            const logs = parsedLogs[threadId];
            if (!logs) {
                alert('没有找到该线程的日志。');
                return;
            }

            // 构建层级结构
            const treeData = buildLogTree(logs, threadId);

            // 清除旧的 SVG
            d3.select("#log-tree").selectAll("*").remove();

            const margin = { top: 20, right: 200, bottom: 20, left: 200 },
                  width = document.getElementById('log-tree').clientWidth - margin.left - margin.right,
                  height = document.getElementById('log-tree').clientHeight - margin.top - margin.bottom;

            const svg = d3.select("#log-tree")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
              .append("g")
                .attr("transform", `translate(${margin.left},${margin.top})`);

            // 创建树布局，使用固定的节点大小
            const treemap = d3.tree()
                .nodeSize([80, 400]); // [垂直间距, 水平间距]

            // 创建层级结构
            const root = d3.hierarchy(treeData, d => d.children);

            // 生成树布局
            treemap(root);

            // Links
            const link = svg.selectAll(".link")
                .data(root.links())
              .enter().append("path")
                .attr("class", "link")
                .attr("d", d3.linkHorizontal()
                    .x(d => d.y)
                    .y(d => d.x))
                .attr("fill", "none")
                .attr("stroke", "#ccc")
                .attr("stroke-width", 2);

            // Nodes
            const node = svg.selectAll(".node")
                .data(root.descendants())
              .enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `translate(${d.y},${d.x})`)
                .on("contextmenu", (event, d) => {
                    event.preventDefault();
                    selectedNode = { type: 'log', id: d.data.uniqueId, data: d.data, tree: 'log' };
                    showContextMenu(event.pageX, event.pageY);
                })
                .on("mouseover", (event, d) => {
                    tooltip.transition().duration(200).style("opacity", .9);
                    tooltip.html(formatTooltip(d.data))
                        .style("left", (event.pageX + 10) + "px")
                        .style("top", (event.pageY - 28) + "px");
                })
                .on("mouseout", () => {
                    tooltip.transition().duration(500).style("opacity", 0);
                });

            // Node rectangles
            node.append("rect")
                .attr("width", 250)
                .attr("height", 60)
                .attr("x", -125)
                .attr("y", -30)
                .attr("rx", 5)
                .attr("ry", 5)
                .attr("fill", d => getNodeColor(d.data.uniqueId))
                .attr("stroke", "#424242")
                .attr("stroke-width", 1)
                .attr("opacity", d => markedNodes.has(d.data.uniqueId) ? 0.5 : 1.0);

            // Node主要内容
            node.append("text")
                .attr("dy", -5)
                .attr("text-anchor", "middle")
                .attr("fill", d => isDarkColor(getNodeColor(d.data.uniqueId)) ? "#ffffff" : "#000000")
                .style("font-weight", "bold")
                .style("pointer-events", "none")
                .text(d => truncateText(d.data.content, 30));

            // Node时间和线程信息
            node.append("text")
                .attr("dy", 15)
                .attr("text-anchor", "middle")
                .attr("fill", "#313131")
                .style("font-size", "10px")
                .text(d => `[${d.data.timestamp}] | 线程:${d.data.threadId}`);

            // 备注显示
            node.append("text")
                .attr("dy", 30)
                .attr("text-anchor", "middle")
                .attr("fill", "#555555")
                .style("font-size", "10px")
                .text(d => {
                    if (nodeStyles[d.data.uniqueId] && nodeStyles[d.data.uniqueId].remark) {
                        return nodeStyles[d.data.uniqueId].remark.length > 15
                            ? nodeStyles[d.data.uniqueId].remark.substring(0, 15) + '...'
                            : nodeStyles[d.data.uniqueId].remark;
                    }
                    return '';
                });

            // 为树添加缩放和平移
            const zoom = d3.zoom()
                .scaleExtent([0.5, 2])
                .on("zoom", (event) => {
                    svg.attr("transform", event.transform);
                });

            d3.select("#log-tree").call(zoom);
        }

        /**
         * 构建日志树结构
         * @param {Array} logs - 日志数组
         * @param {string} threadId - 线程ID，用于唯一标识
         * @returns {Object} - 树形结构
         */
        function buildLogTree(logs, threadId) {
            const root = { 
                id: `log-root-${threadId}`, 
                uniqueId: `log-root-${threadId}`,
                content: "根", 
                children: [], 
                level: "I" 
            };
            let currentNode = root;
            let stack = [root];
            let nodeIndex = 1;

            for (const log of logs) {
                const isEntry = /Entered\.\.\.$/.test(log.content);
                const isExit = /Exited\.\.\.$/.test(log.content);

                if (isEntry) {
                    const newNode = {
                        id: `log-${threadId}-${nodeIndex}`,
                        uniqueId: `log-${threadId}-${nodeIndex}`,
                        content: log.content.replace(/ Entered\.\.\.$/, ''),
                        timestamp: log.timestamp,
                        threadId: log.threadId,
                        level: log.level,
                        children: []
                    };
                    currentNode.children.push(newNode);
                    stack.push(newNode);
                    currentNode = newNode;
                    nodeIndex++;
                } else if (isExit) {
                    if (stack.length > 1) {
                        stack.pop();
                        currentNode = stack[stack.length - 1];
                    }
                } else {
                    const newNode = {
                        id: `log-${threadId}-${nodeIndex}`,
                        uniqueId: `log-${threadId}-${nodeIndex}`,
                        content: log.content,
                        timestamp: log.timestamp,
                        threadId: log.threadId,
                        level: log.level,
                        children: []
                    };
                    currentNode.children.push(newNode);
                    nodeIndex++;
                }
            }

            return root;
        }

        /**
         * 格式化 Tooltip 内容
         * @param {Object} data - 节点数据
         * @returns {string} - Tooltip 的 HTML 内容
         */
        function formatTooltip(data) {
            return `<strong>时间:</strong> ${data.timestamp}<br/>
                    <strong>级别:</strong> ${data.level}<br/>
                    <strong>内容:</strong> ${data.content}</br/>
                    <strong>备注:</strong> ${nodeStyles[data.uniqueId] ? nodeStyles[data.uniqueId].remark : '无' }<br/>`;
        }

        /**
         * 获取节点颜色根据节点样式或默认
         * @param {string} nodeId - 节点 ID（带前缀）
         * @returns {string} - 颜色代码
         */
        function getNodeColor(nodeId) {
            if (nodeStyles[nodeId] && nodeStyles[nodeId].color) {
                return nodeStyles[nodeId].color;
            }
            // 默认颜色根据类型
            if (nodeId.startsWith('thread-')) { // 线程节点
                return "#4FC3F7";
            } else if (nodeId.startsWith('log-')) { // 日志节点
                const log = getLogByUniqueId(nodeId);
                if (log) {
                    return log.level === 'E' ? '#e57373' : '#64b5f6';
                }
            }
            return '#BBDEFB';
        }

        /**
         * 根据唯一ID获取日志对象
         * @param {string} uniqueId 
         * @returns {Object|null}
         */
        function getLogByUniqueId(uniqueId) {
            // uniqueId格式: log-<threadId>-<index>
            const parts = uniqueId.split('-');
            if (parts.length < 3) return null;
            const threadId = parts[1];
            const index = parseInt(parts[2]);
            const logs = parsedLogs[threadId];
            if (logs && logs[index - 1]) {
                return logs[index - 1];
            }
            return null;
        }

        /**
         * 判断颜色是否为深色
         * @param {string} hex - 颜色代码
         * @returns {boolean}
         */
        function isDarkColor(hex) {
            const r = parseInt(hex.slice(1,3),16);
            const g = parseInt(hex.slice(3,5),16);
            const b = parseInt(hex.slice(5,7),16);
            const brightness = (r * 299 + g * 587 + b * 114) / 1000;
            return brightness < 125;
        }

        /**
         * 截断文本并添加省略号
         * @param {string} text
         * @param {number} maxLength
         * @returns {string}
         */
        function truncateText(text, maxLength) {
            return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
        }

        /**
         * 显示自定义右键菜单
         * @param {number} x - 鼠标点击的 X 坐标
         * @param {number} y - 鼠标点击的 Y 坐标
         */
        function showContextMenu(x, y) {
            contextMenu.style.left = `${x}px`;
            contextMenu.style.top = `${y}px`;
            contextMenu.style.display = 'block';
        }

        /**
         * 隐藏自定义右键菜单
         */
        function hideContextMenu() {
            contextMenu.style.display = 'none';
        }

        /**
         * 切换节点的重要性（透明度）
         */
        function toggleNodeImportance() {
            if (!selectedNode.id) return;
            if (markedNodes.has(selectedNode.id)) {
                markedNodes.delete(selectedNode.id);
            } else {
                markedNodes.add(selectedNode.id);
            }

            if (selectedNode.type === 'log') {
                // 更新日志树节点的透明度
                d3.selectAll("#log-tree .node")
                    .filter(d => d.data.uniqueId === selectedNode.id)
                    .select("rect")
                    .transition()
                    .duration(300)
                    .attr("opacity", markedNodes.has(selectedNode.id) ? 0.2 : 1.0);
            } else if (selectedNode.type === 'thread') {
                // 更新线程树节点的透明度
                d3.selectAll("#thread-tree .node")
                    .filter(d => `thread-${d.data.id}` === selectedNode.id)
                    .select("rect")
                    .transition()
                    .duration(300)
                    .attr("opacity", markedNodes.has(selectedNode.id) ? 0.5 : 1.0);
            }
            hideContextMenu();
        }

        /**
         * 添加或编辑备注
         */
        function addOrEditRemark() {
            if (!selectedNode.id) return;
            // 预填充备注
            if (nodeStyles[selectedNode.id] && nodeStyles[selectedNode.id].remark) {
                remarkText.value = nodeStyles[selectedNode.id].remark;
                selectedColor = nodeStyles[selectedNode.id].color || "#FFFFFF";
            } else {
                remarkText.value = "";
                selectedColor = "#FFFFFF";
            }
            // 预选颜色
            document.querySelectorAll('.color-swatch').forEach(swatch => {
                if (swatch.getAttribute('data-color') === selectedColor) {
                    swatch.classList.add('selected');
                } else {
                    swatch.classList.remove('selected');
                }
            });
            // 显示弹窗
            remarkModal.style.display = 'block';
        }

        /**
         * 保存备注
         */
        function saveRemark() {
            const remark = remarkText.value.trim();
            nodeStyles[selectedNode.id] = nodeStyles[selectedNode.id] || {};
            nodeStyles[selectedNode.id].remark = remark;
            nodeStyles[selectedNode.id].color = selectedColor;

            // 更新节点颜色和备注显示
            if (selectedNode.type === 'log') {
                d3.selectAll("#log-tree .node")
                    .filter(d => d.data.uniqueId === selectedNode.id)
                    .select("rect")
                    .transition()
                    .duration(300)
                    .attr("fill", getNodeColor(selectedNode.id));

                // 更新备注显示
                d3.selectAll("#log-tree .node")
                    .filter(d => d.data.uniqueId === selectedNode.id)
                    .select("text:nth-child(3)")
                    .text(d => {
                        if (nodeStyles[selectedNode.id] && nodeStyles[selectedNode.id].remark) {
                            return nodeStyles[selectedNode.id].remark.length > 15
                                ? nodeStyles[selectedNode.id].remark.substring(0, 15) + '...'
                                : nodeStyles[selectedNode.id].remark;
                        }
                        return '';
                    });
            } else if (selectedNode.type === 'thread') {
                d3.selectAll("#thread-tree .node")
                    .filter(d => `thread-${d.data.id}` === selectedNode.id)
                    .select("rect")
                    .transition()
                    .duration(300)
                    .attr("fill", getNodeColor(selectedNode.id));

                // 更新备注显示
                d3.selectAll("#thread-tree .node")
                    .filter(d => `thread-${d.data.id}` === selectedNode.id)
                    .select("text:nth-child(2)")
                    .text(d => {
                        if (nodeStyles[selectedNode.id] && nodeStyles[selectedNode.id].remark) {
                            return nodeStyles[selectedNode.id].remark.length > 15
                                ? nodeStyles[selectedNode.id].remark.substring(0, 15) + '...'
                                : nodeStyles[selectedNode.id].remark;
                        }
                        return '';
                    });
            }

            // 关闭弹窗
            remarkModal.style.display = 'none';
        }

        /**
         * 取消备注编辑
         */
        function cancelRemark() {
            remarkModal.style.display = 'none';
        }

        /**
         * 颜色选择
         */
        colorPalette.addEventListener('click', function(event) {
            if (event.target.classList.contains('color-swatch')) {
                document.querySelectorAll('.color-swatch').forEach(swatch => swatch.classList.remove('selected'));
                event.target.classList.add('selected');
                selectedColor = event.target.getAttribute('data-color');
            }
        });

        // 右键菜单选项事件
        toggleImportanceMenu.addEventListener('click', toggleNodeImportance);
        addRemarkMenu.addEventListener('click', addOrEditRemark);

        // 备注弹窗按钮事件
        saveRemarkButton.addEventListener('click', saveRemark);
        cancelRemarkButton.addEventListener('click', cancelRemark);

        // 点击其他地方隐藏右键菜单
        document.addEventListener('click', function(e) {
            if (e.button !== 2) { // 非右键点击
                hideContextMenu();
            }
        });

        // 导出 JSON
        document.getElementById('export-json').addEventListener('click', function() {
            const data = {
                parsedLogs,
                threadParents,
                nodeStyles
            };
            const blob = new Blob([JSON.stringify(data, null, 2)], {type: "application/json"});
            saveAs(blob, "log_data.json");
        });

        // 导入 JSON
        document.getElementById('import-json').addEventListener('change', function(event) {
            const file = event.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const data = JSON.parse(e.target.result);
                    parsedLogs = data.parsedLogs || {};
                    threadParents = data.threadParents || {};
                    nodeStyles = data.nodeStyles || {};
                    // 更新 markedNodes
                    markedNodes = new Set(Object.keys(nodeStyles).filter(key => nodeStyles[key].marked));
                    renderThreadTree();
                    clearLogTree();
                    alert('导入成功！');
                } catch (err) {
                    alert('导入失败，文件格式不正确。');
                }
            };
            reader.readAsText(file);
        });

        // 导出线程关系为图片
        document.getElementById('export-threads').addEventListener('click', function() {
            const svgElement = document.getElementById('thread-tree');
            exportSVGAsPng(svgElement, '线程关系.png');
        });

        // 导出日志跟踪为图片
        document.getElementById('export-logs').addEventListener('click', function() {
            const svgElement = document.getElementById('log-tree');
            exportSVGAsPng(svgElement, '日志跟踪.png');
        });

        /**
         * 导出 SVG 为 PNG 图片
         * @param {HTMLElement} svgElement 
         * @param {string} filename 
         */
        function exportSVGAsPng(svgElement, filename) {
            const serializer = new XMLSerializer();
            const svgString = serializer.serializeToString(svgElement);
            const canvas = document.createElement('canvas');
            const bbox = svgElement.getBBox();
            canvas.width = bbox.width + 400; // 加大画布以适应内容
            canvas.height = bbox.height + 400;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = "#ffffff"; // 设置白色背景
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            const img = new Image();
            const svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
            const url = URL.createObjectURL(svgBlob);
            img.onload = function() {
                ctx.drawImage(img, 200, 200); // 偏移绘制以适应大画布
                URL.revokeObjectURL(url);
                canvas.toBlob(function(blob) {
                    saveAs(blob, filename);
                });
            };
            img.src = url;
        }

        // 处理窗口大小变化
        window.addEventListener('resize', function() {
            if (Object.keys(parsedLogs).length > 0) {
                renderThreadTree();
                // 如果已经选择了线程，则重新渲染日志树
                if (currentSelectedThreadId) {
                    renderLogTree(currentSelectedThreadId);
                }
            }
        });

        /**
         * 监听全局右键菜单关闭
         */
        window.addEventListener('click', hideContextMenu);
        window.addEventListener('scroll', hideContextMenu);

        // Ensure that when the modal is open, clicking outside doesn't close it unintentionally
        remarkModal.addEventListener('click', function(event) {
            if (event.target === remarkModal) {
                remarkModal.style.display = 'none';
            }
        });
    </script>
</body>
</html>