{"id":27064,"date":"2025-06-25T12:08:53","date_gmt":"2025-06-25T04:08:53","guid":{"rendered":"https:\/\/www.hkmu.edu.hk\/oetools\/?page_id=27064"},"modified":"2025-11-03T17:05:31","modified_gmt":"2025-11-03T09:05:31","slug":"agent5mindmapcreator","status":"publish","type":"page","link":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/","title":{"rendered":"Agent 5 Mind Map Creator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"27064\" class=\"elementor elementor-27064\" data-elementor-settings=\"[]\">\n\t\t\t\t\t\t\t<div class=\"elementor-section-wrap\">\n\t\t\t\t\t\t\t<section class=\"has_eae_slider wavo-column-gap-default elementor-section elementor-top-section elementor-element elementor-element-c30ef03 elementor-section-full_width elementor-section-height-default elementor-section-height-default\" data-id=\"c30ef03\" data-element_type=\"section\" data-settings=\"{&quot;jet_parallax_layout_list&quot;:[]}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"has_eae_slider elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-1e20c56\" data-id=\"1e20c56\" data-element_type=\"column\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-c6e85b6 elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"c6e85b6\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n    <title>Mind Map Creator<\/title>\r\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.min.js\"><\/script>\r\n    <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/markmap-autoloader@0.18\"><\/script>\r\n    <script src=\"https:\/\/unpkg.com\/mammoth@1.4.8\/mammoth.browser.min.js\"><\/script>\r\n    <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\"><\/script>\r\n    <style>\r\n        \/* ===== GLOBAL STYLES ===== *\/\r\n        .mindmap-widget * {\r\n            margin: 0 !important;\r\n            padding: 0 !important;\r\n            box-sizing: border-box !important;\r\n            border: none !important;\r\n            outline: none !important;\r\n        }\r\n\r\n        .mindmap-widget {\r\n            all: initial !important;\r\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;\r\n            line-height: 1.6 !important;\r\n            width: 100% !important;\r\n            max-width: none !important;\r\n            margin: 0 !important;\r\n            padding: 20px 0 !important;\r\n            position: relative !important;\r\n            display: block !important;\r\n            background: #fa975e !important;\r\n            color: #333 !important;\r\n        }\r\n        \r\n        .mindmap-widget .container {\r\n            max-width: 1600px !important;\r\n            min-height: 600px !important;\r\n            margin: 0px auto !important;\r\n            background: rgba(255, 255, 255, 0.95) !important;\r\n            border-radius: 12px !important;\r\n            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1) !important;\r\n            overflow: hidden !important;\r\n            position: relative !important;\r\n        }\r\n        \r\n        .mindmap-widget .header {\r\n            background: linear-gradient(45deg, #fa975e, #e8864a) !important;\r\n            color: #ffffff !important;\r\n            padding: 20px !important;\r\n            text-align: center !important;\r\n        }\r\n\r\n        .mindmap-widget .header h1 {\r\n            font-size: 2.5rem !important;\r\n            font-weight: 700 !important;\r\n            margin-bottom: 10px !important;\r\n            color: #ffffff !important;\r\n            position: relative !important;\r\n            text-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;\r\n        }\r\n\r\n        .mindmap-widget .header p {\r\n            font-size: 1.2rem !important;\r\n            color: #ffffff !important;\r\n            opacity: 0.9 !important;\r\n            max-width: 800px !important;\r\n            margin: 0 auto !important;\r\n            position: relative !important;\r\n        }\r\n        \r\n        .mindmap-widget .section {\r\n            padding: 20px !important;\r\n            border-bottom: 1px solid #eee !important;\r\n            position: relative !important;\r\n        }\r\n        \r\n        .mindmap-widget .section:last-child {\r\n            border-bottom: none !important;\r\n        }\r\n        \r\n        .mindmap-widget .section h3 {\r\n            margin-bottom: 15px !important;\r\n            color: #fa975e !important;\r\n            cursor: pointer !important;\r\n            position: relative !important;\r\n            padding-right: 20px !important;\r\n            font-size: 24px !important;\r\n            font-weight: 700 !important;\r\n        }\r\n        \r\n        .mindmap-widget .section h3::after {\r\n            content: '\u25bc' !important;\r\n            position: absolute !important;\r\n            right: 0 !important;\r\n            font-size: 0px !important;\r\n            transition: transform 0.3s !important;\r\n        }\r\n        \r\n        .mindmap-widget .section.collapsed h3::after {\r\n            transform: rotate(-90deg) !important;\r\n        }\r\n        \r\n        .mindmap-widget .drop-area {\r\n            border: 2px dashed #fa975e !important;\r\n            border-radius: 8px !important;\r\n            padding: 30px !important;\r\n            text-align: center !important;\r\n            background: rgba(250, 151, 94, 0.05) !important;\r\n            cursor: pointer !important;\r\n            transition: all 0.3s ease !important;\r\n            margin-bottom: 0px !important;\r\n        }\r\n        \r\n        .mindmap-widget .drop-area.active {\r\n            background: rgba(250, 151, 94, 0.15) !important;\r\n            border-color: #e8864a !important;\r\n        }\r\n        \r\n        .mindmap-widget .pdf-info {\r\n            background: #f8f9fa !important;\r\n            border-radius: 8px !important;\r\n            padding: 15px !important;\r\n            margin: 15px 0 !important;\r\n            border-left: 4px solid #fa975e !important;\r\n        }\r\n        \r\n        .mindmap-widget .pdf-info h4 {\r\n            color: #fa975e !important;\r\n            margin-bottom: 10px !important;\r\n            font-size: 1.1rem !important;\r\n            font-weight: 600 !important;\r\n        }\r\n        \r\n        .mindmap-widget .markdown-editor {\r\n            width: 100% !important;\r\n            min-height: 300px !important;\r\n            padding: 15px !important;\r\n            border: 1px solid #ddd !important;\r\n            border-radius: 8px !important;\r\n            font-family: 'Courier New', monospace !important;\r\n            font-size: 14px !important;\r\n            line-height: 1.5 !important;\r\n            resize: vertical !important;\r\n            background: #ffffff !important;\r\n        }\r\n        \r\n        .mindmap-widget .mindmap-container {\r\n            border: 1px solid #ddd !important;\r\n            border-radius: 8px !important;\r\n            overflow: hidden !important;\r\n            background: #ffffff !important;\r\n            height: 70vh !important;\r\n            margin-top: 15px !important;\r\n            position: relative !important;\r\n        }\r\n        \r\n        \/* Direct markmap integration styles *\/\r\n        .mindmap-widget .markmap {\r\n            width: 100% !important;\r\n            height: 100% !important;\r\n            display: block !important;\r\n        }\r\n\r\n        .mindmap-widget .markmap svg {\r\n            width: 100% !important;\r\n            height: 100% !important;\r\n            display: block !important;\r\n        }\r\n\r\n        .mindmap-widget .markmap-content {\r\n            display: none !important;\r\n        }\r\n        \r\n        .mindmap-widget .generate-btn {\r\n            background: #7AC143 !important;\r\n            color: #ffffff !important;\r\n            border: none !important;\r\n            padding: 8px 28px !important;\r\n            border-radius: 8px !important;\r\n            cursor: pointer !important;\r\n            font-size: 24px !important;\r\n            font-weight: 600 !important;\r\n            width: 100% !important;\r\n            margin-top: 20px !important;\r\n            transition: background 0.3s !important;\r\n        }\r\n        \r\n        .mindmap-widget .generate-btn:hover:not(:disabled) {\r\n            background: #5A8F32 !important;\r\n        }\r\n        \r\n        .mindmap-widget .generate-btn:disabled {\r\n            background: #ccc !important;\r\n            cursor: not-allowed !important;\r\n        }\r\n        \r\n        .mindmap-widget .status {\r\n            padding: 10px !important;\r\n            border-radius: 6px !important;\r\n            margin: 10px 0 !important;\r\n            text-align: center !important;\r\n            font-weight: 500 !important;\r\n            max-width: 100% !important;\r\n            width: 100% !important;\r\n            position: relative !important;\r\n            display: block !important;\r\n            font-size: 14px !important;\r\n            line-height: 1.4 !important;\r\n        }\r\n        \r\n        .mindmap-widget .status.loading {\r\n            background: #fff3cd !important;\r\n            color: #856404 !important;\r\n            border: 1px solid #ffeaa7 !important;\r\n        }\r\n        \r\n        .mindmap-widget .status.success {\r\n            background: #d4edda !important;\r\n            color: #155724 !important;\r\n            border: 1px solid #c3e6cb !important;\r\n        }\r\n        \r\n        .mindmap-widget .status.error {\r\n            background: #f8d7da !important;\r\n            color: #721c24 !important;\r\n            border: 1px solid #f5c6cb !important;\r\n        }\r\n        \r\n        .mindmap-widget .hidden {\r\n            display: none !important;\r\n        }\r\n        \r\n        .mindmap-widget #fileInput {\r\n            display: none !important;\r\n        }\r\n        \r\n        \/* File info styles *\/\r\n        .mindmap-widget .file-details {\r\n            background: #ffffff !important;\r\n            padding: 15px !important;\r\n            border-radius: 8px !important;\r\n            margin-bottom: 15px !important;\r\n            box-shadow: 0 2px 4px rgba(0,0,0,0.05) !important;\r\n            display: flex !important;\r\n            flex-direction: row !important;\r\n            flex-wrap: wrap !important;\r\n            gap: 20px 30px !important;\r\n            align-items: center !important;\r\n        }\r\n        \r\n        .mindmap-widget .file-detail-row {\r\n            display: flex !important;\r\n            flex-direction: row !important;\r\n            align-items: center !important;\r\n            gap: 8px !important;\r\n            margin-bottom: 0 !important;\r\n            flex-shrink: 0 !important;\r\n        }\r\n        \r\n        .mindmap-widget .file-detail-row.hidden {\r\n            display: none !important;\r\n        }\r\n        \r\n        .mindmap-widget .file-detail-label {\r\n            font-weight: 600 !important;\r\n            color: #666 !important;\r\n            font-size: 14px !important;\r\n            white-space: nowrap !important;\r\n        }\r\n        \r\n        .mindmap-widget .file-detail-value {\r\n            color: #333 !important;\r\n            font-size: 14px !important;\r\n            white-space: nowrap !important;\r\n        }\r\n\r\n        @media (max-width: 768px) {\r\n            .mindmap-widget .container {\r\n                margin: 0 10px !important;\r\n            }\r\n            \r\n            .mindmap-widget .file-details {\r\n                flex-direction: column !important;\r\n                align-items: flex-start !important;\r\n                gap: 12px !important;\r\n            }\r\n            \r\n            .mindmap-widget .file-detail-row {\r\n                width: 100% !important;\r\n                flex-direction: row !important;\r\n                justify-content: space-between !important;\r\n            }\r\n            \r\n            .mindmap-widget .file-detail-label {\r\n                margin-bottom: 0 !important;\r\n            }\r\n        }\r\n        \r\n        @media (max-width: 480px) {\r\n            .mindmap-widget .file-details {\r\n                gap: 10px 15px !important;\r\n            }\r\n            \r\n            .mindmap-widget .file-detail-row {\r\n                flex-wrap: wrap !important;\r\n            }\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n    <div class=\"mindmap-widget\">\r\n        <div class=\"container\">\r\n            <!-- Header -->\r\n            <div class=\"header\">\r\n                <h1>Interactive Mind Map Designer<\/h1>\r\n                <p>Instantly visualize document structure as a colorful, clickable mind map. Expand or collapse nodes to explore relationships and strengthen concept retention.<\/p>\r\n            <\/div>\r\n\r\n            <!-- Step 1: Document Upload -->\r\n            <div class=\"section\">\r\n                <h3>Step 1: Upload Document<\/h3>\r\n                <div class=\"drop-area\" id=\"dropArea\">\r\n                    <p>Drag & drop document here<\/p>\r\n                    <p><u>or click to select<\/u><\/p>\r\n                    <p style=\"font-size: 0.9rem; margin-top: 10px; color: #666;\">\r\n                        Supported formats: PDF, DOCX, PPTX, TXT, HTML, MD, XML, JSON, CSV\r\n                    <\/p>\r\n                <\/div>\r\n                <input type=\"file\" id=\"fileInput\" accept=\".pdf,.docx,.pptx,.txt,.html,.htm,.md,.xml,.json,.csv\">\r\n                \r\n                <!-- File Information Display -->\r\n                <div class=\"pdf-info hidden\" id=\"fileInfo\">\r\n                    <h4>File Successfully Loaded<\/h4>\r\n                    \r\n                    <!-- File Details -->\r\n                    <div class=\"file-details\">\r\n                        <div class=\"file-detail-row\">\r\n                            <div class=\"file-detail-label\">File Name:<\/div>\r\n                            <div class=\"file-detail-value\" id=\"fileNameDisplay\"><\/div>\r\n                        <\/div>\r\n                        <div class=\"file-detail-row\">\r\n                            <div class=\"file-detail-label\">File Type:<\/div>\r\n                            <div class=\"file-detail-value\" id=\"fileTypeDisplay\"><\/div>\r\n                        <\/div>\r\n                        <div class=\"file-detail-row\">\r\n                            <div class=\"file-detail-label\">File Size:<\/div>\r\n                            <div class=\"file-detail-value\" id=\"fileSizeDisplay\"><\/div>\r\n                        <\/div>\r\n                        <div class=\"file-detail-row\">\r\n                            <div class=\"file-detail-label\">Character Count:<\/div>\r\n                            <div class=\"file-detail-value\" id=\"charCountDisplay\"><\/div>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <button class=\"generate-btn\" id=\"generateKeywordsBtn\" disabled>Extract Keywords<\/button>\r\n                <div id=\"status\"><\/div>\r\n            <\/div>\r\n\r\n            <!-- Step 2: Review Keywords -->\r\n            <div class=\"section hidden\" id=\"keywordsSection\">\r\n                <h3>Step 2: Review Keywords and Definitions Extracted<\/h3>\r\n                <p style=\"margin-bottom: 15px; color: #666;\">Review and edit the extracted keywords and definitions below:<\/p>\r\n                \r\n                <textarea class=\"markdown-editor\" id=\"markdownEditor\" placeholder=\"Keywords and definitions will appear here...\"><\/textarea>\r\n                \r\n                <button class=\"generate-btn\" id=\"generateMindmapBtn\">Generate Mindmap<\/button>\r\n            <\/div>\r\n\r\n            <!-- Step 3: Generated Mindmap -->\r\n            <div class=\"section hidden\" id=\"mindmapSection\">\r\n                <h3>Step 3: Generated Mindmap<\/h3>\r\n                <p style=\"margin-bottom: 15px; color: #666;\">Your interactive mindmap: click to expand\/collapse nodes, drag to pan, zoom with mouse wheel.<\/p>\r\n                \r\n                <!-- Direct markmap integration (WordPress-compatible) -->\r\n                <div class=\"mindmap-container\">\r\n                    <div class=\"markmap\" id=\"mindmapContainer\">\r\n                        <div class=\"markmap-content\" id=\"mindmapContent\"><\/div>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <script>\r\n        \/\/ =====================================\r\n        \/\/ BACKEND AUTHORISATION SYSTEM\r\n        \/\/ =====================================\r\n        function modInverse(k1, mod = 256) {\r\n            let t = 0, newT = 1;\r\n            let r = mod, newR = k1;\r\n            \r\n            while (newR !== 0) {\r\n                const quotient = Math.floor(r \/ newR);\r\n                [t, newT] = [newT, t - quotient * newT];\r\n                [r, newR] = [newR, r - quotient * newR];\r\n            }\r\n            \r\n            if (r > 1) throw new Error('k1 is not invertible');\r\n            if (t < 0) t += mod;\r\n            return t;\r\n        }\r\n\r\n        function decryptServerPayload(payload) {\r\n            if (payload.length < 12) {\r\n                throw new Error('Invalid payload: too short');\r\n            }\r\n            \r\n            const suffix = payload.slice(-12);\r\n            const numericKeyStr = suffix.slice(0, 10);\r\n            const hourMinus8 = suffix.slice(10, 12);\r\n            const b64 = payload.slice(0, -12);\r\n            \r\n            const numericKey = BigInt(numericKeyStr);\r\n            const k1 = Number((numericKey % 127n) * 2n + 1n);\r\n            const k2 = Number(numericKey % 256n);\r\n            \r\n            const invK1 = modInverse(k1, 256);\r\n            \r\n            const binaryString = atob(b64);\r\n            const bytes = new Uint8Array(binaryString.length);\r\n            for (let i = 0; i < binaryString.length; i++) {\r\n                bytes[i] = binaryString.charCodeAt(i);\r\n            }\r\n            \r\n            const decryptedBytes = new Uint8Array(bytes.length);\r\n            for (let i = 0; i < bytes.length; i++) {\r\n                const c = bytes[i];\r\n                const temp = (c - k2 + 256) % 256;\r\n                decryptedBytes[i] = (invK1 * temp) % 256;\r\n            }\r\n            \r\n            const decoder = new TextDecoder('utf-8');\r\n            return decoder.decode(decryptedBytes);\r\n        }\r\n\r\n        async function fetchDecryptedKey(service) {\r\n            const sanitizedBase = API_BASE_URL.endsWith('\/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;\r\n            const url = `${sanitizedBase}\/get_encrypted_key?service=${service}`;\r\n            \r\n            try {\r\n                const response = await fetch(url, {\r\n                    method: 'GET',\r\n                    headers: {\r\n                        'Accept': 'application\/json'\r\n                    }\r\n                });\r\n                \r\n                if (!response.ok) {\r\n                    const errorText = await response.text();\r\n                    throw new Error(`Failed to fetch ${service} key: ${response.status} ${errorText}`);\r\n                }\r\n                \r\n                const data = await response.json();\r\n                \r\n                if (!data.encrypted) {\r\n                    throw new Error(`No encrypted key returned for ${service}`);\r\n                }\r\n                \r\n                const decryptedKey = decryptServerPayload(data.encrypted);\r\n                return decryptedKey;\r\n                \r\n            } catch (error) {\r\n                console.error(`Error fetching ${service} key:`, error);\r\n                throw error;\r\n            }\r\n        }\r\n\r\n        async function initializeApiKeys() {\r\n            try {\r\n                const cozeKey = await fetchDecryptedKey('coze');\r\n                return { cozeKey };\r\n            } catch (error) {\r\n                console.error('Failed to initialize API keys:', error);\r\n                throw error;\r\n            }\r\n        }\r\n\r\n        function clearApiKeys() {\r\n            if (typeof COZE_API_KEY_RUNTIME !== 'undefined') {\r\n                COZE_API_KEY_RUNTIME = null;\r\n            }\r\n            console.log('API keys cleared from memory');\r\n        }\r\n\r\n        async function retryWithKeyRefresh(apiCallFn, maxRetries = 3) {\r\n            let lastError;\r\n            \r\n            for (let attempt = 1; attempt <= maxRetries; attempt++) {\r\n                try {\r\n                    const result = await apiCallFn();\r\n                    return result;\r\n                } catch (error) {\r\n                    lastError = error;\r\n                    \r\n                    const is401or403 = error.message.includes('401') || \r\n                                       error.message.includes('403') ||\r\n                                       error.message.includes('HTTP 401') ||\r\n                                       error.message.includes('HTTP 403');\r\n                    \r\n                    if (is401or403 && attempt < maxRetries) {\r\n                        console.warn(`API call failed with auth error (attempt ${attempt}\/${maxRetries}), refreshing coze key...`);\r\n                        \r\n                        try {\r\n                            const newKey = await fetchDecryptedKey('coze');\r\n                            COZE_API_KEY_RUNTIME = newKey;\r\n                            console.log(`\u2713 coze key refreshed, retrying...`);\r\n                        } catch (refreshError) {\r\n                            console.error(`Failed to refresh coze key:`, refreshError);\r\n                            throw refreshError;\r\n                        }\r\n                    } else {\r\n                        throw lastError;\r\n                    }\r\n                }\r\n            }\r\n            \r\n            throw lastError;\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ Configuration & Global Variables\r\n        \/\/ =====================================\r\n        \r\n\r\n        let COZE_API_KEY_RUNTIME = null;\r\n\r\n        \/\/ API key getter\r\n        Object.defineProperty(window, 'COZE_API_KEY', {\r\n            get() { return COZE_API_KEY_RUNTIME; },\r\n            configurable: false\r\n        });\r\n\r\n        const API_BASE_URL = (() => {\r\n            try {\r\n                const { protocol, hostname } = window.location;\r\n                if (\r\n                    protocol === 'https:' &&\r\n                    (hostname === 'oetools.net' || hostname.endsWith('.oetools.net'))\r\n                ) {\r\n                    return '\/api';\r\n                }\r\n            } catch (error) {\r\n                console.warn('API base detection fallback to public endpoint:', error);\r\n            }\r\n            return 'https:\/\/oetools.net\/api';\r\n        })();\r\n\r\n        \/\/ Coze API Configuration - Enhanced Multi-Bot Setup (Up to 10 Bots)\r\n        const COZE_CONFIG = {\r\n            API_URL: 'https:\/\/api.coze.com\/open_api\/v2\/chat',\r\n            BOT_IDS: [\r\n                '7520104374417342465', \/\/ BOT_ID_1\r\n                '7523067373579386888', \/\/ BOT_ID_2\r\n                '7523066382184595474', \/\/ BOT_ID_3\r\n                '7523148300271845384', \/\/ BOT_ID_4\r\n                '7523147391647105031', \/\/ BOT_ID_5\r\n                '7523152353541881864', \/\/ BOT_ID_6\r\n                '7523152353542144008', \/\/ BOT_ID_7\r\n                '7523151034505101320', \/\/ BOT_ID_8\r\n                '7523153579986026514', \/\/ BOT_ID_9\r\n                '7523158893183320082'  \/\/ BOT_ID_10\r\n            ],\r\n            USER_ID: 'mindmap_user_' + Date.now(),\r\n            CHUNK_SIZE: 40000,       \/\/ 40k characters per bot~=7000 English Words\r\n            OVERLAP_RATIO: 0.1,      \/\/ 10% overlap between chunks\r\n            MAX_RETRIES: 2           \/\/ Maximum retry attempts per bot\r\n        };\r\n\r\n        \/\/ Set up PDF.js worker\r\n        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.worker.min.js';\r\n\r\n        \/\/ DOM Elements\r\n        const dropArea = document.getElementById('dropArea');\r\n        const fileInput = document.getElementById('fileInput');\r\n        const generateKeywordsBtn = document.getElementById('generateKeywordsBtn');\r\n        const generateMindmapBtn = document.getElementById('generateMindmapBtn');\r\n        const statusDiv = document.getElementById('status');\r\n        const fileInfo = document.getElementById('fileInfo');\r\n        const keywordsSection = document.getElementById('keywordsSection');\r\n        const mindmapSection = document.getElementById('mindmapSection');\r\n        const markdownEditor = document.getElementById('markdownEditor');\r\n        const mindmapContainer = document.getElementById('mindmapContainer');\r\n        const mindmapContent = document.getElementById('mindmapContent');\r\n        \r\n        \/\/ File info elements\r\n        const fileNameDisplay = document.getElementById('fileNameDisplay');\r\n        const fileTypeDisplay = document.getElementById('fileTypeDisplay');\r\n        const fileSizeDisplay = document.getElementById('fileSizeDisplay');\r\n        const charCountDisplay = document.getElementById('charCountDisplay');\r\n\r\n        \/\/ Global variables for multi-bot content processing\r\n        let fileContent = '';\r\n        let contentParts = []; \r\n        let extractedKeywords = '';\r\n        let currentFileName = '';\r\n        let currentFileExtension = ''; \/\/ Store current file extension\r\n        let currentWordCount = 0; \/\/ Store current word count\r\n        \r\n        \/\/ Supported file types\r\n        const SUPPORTED_TYPES = ['pdf', 'docx', 'pptx', 'txt', 'html', 'htm', 'md', 'xml', 'json', 'csv'];\r\n\r\n        \/\/ =====================================\r\n        \/\/ File Processing Functions\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Get file extension from filename\r\n         *\/\r\n        function getFileExtension(filename) {\r\n            return filename.split('.').pop().toLowerCase();\r\n        }\r\n        \r\n        \/**\r\n         * Format file size in human-readable format\r\n         *\/\r\n        function formatFileSize(bytes) {\r\n            if (bytes === 0) return '0 Bytes';\r\n            const k = 1024;\r\n            const sizes = ['Bytes', 'KB', 'MB', 'GB'];\r\n            const i = Math.floor(Math.log(bytes) \/ Math.log(k));\r\n            return parseFloat((bytes \/ Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\r\n        }\r\n\r\n        \/**\r\n         * Update visibility of file info rows based on their values\r\n         *\/\r\n        function updateFileInfoVisibility() {\r\n            const fileDetailRows = document.querySelectorAll('.mindmap-widget .file-detail-row');\r\n            fileDetailRows.forEach(row => {\r\n                const valueElement = row.querySelector('.file-detail-value');\r\n                if (valueElement) {\r\n                    const value = valueElement.textContent.trim();\r\n                    if (value === '-' || value === 'N\/A' || value === '') {\r\n                        row.classList.add('hidden');\r\n                    } else {\r\n                        row.classList.remove('hidden');\r\n                    }\r\n                }\r\n            });\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from DOCX files\r\n         *\/\r\n        async function extractDocxText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const result = await mammoth.extractRawText({ arrayBuffer });\r\n                return result.value;\r\n            } catch (error) {\r\n                throw new Error('Failed to read DOCX file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from PPTX files\r\n         *\/\r\n        async function extractPptxText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const zip = await JSZip.loadAsync(arrayBuffer);\r\n                let text = '';\r\n                \r\n                \/\/ Extract text from slides\r\n                const slideFiles = Object.keys(zip.files).filter(name => \r\n                    name.startsWith('ppt\/slides\/slide') && name.endsWith('.xml')\r\n                );\r\n                \r\n                for (const slideFile of slideFiles) {\r\n                    const content = await zip.file(slideFile).async('string');\r\n                    \/\/ Simple XML parsing to extract text\r\n                    const textMatches = content.match(\/<a:t[^>]*>([^<]*)<\\\/a:t>\/g);\r\n                    if (textMatches) {\r\n                        textMatches.forEach(match => {\r\n                            const textContent = match.replace(\/<[^>]*>\/g, '');\r\n                            if (textContent.trim()) {\r\n                                text += textContent + '\\n';\r\n                            }\r\n                        });\r\n                    }\r\n                }\r\n                \r\n                return text;\r\n            } catch (error) {\r\n                throw new Error('Failed to read PPTX file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Extract text from PDF files\r\n         *\/\r\n        async function extractPdfText(file) {\r\n            try {\r\n                const arrayBuffer = await file.arrayBuffer();\r\n                const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;\r\n                \r\n                let fullText = '';\r\n                for (let i = 1; i <= pdf.numPages; i++) {\r\n                    const page = await pdf.getPage(i);\r\n                    const textContent = await page.getTextContent();\r\n                    fullText += textContent.items.map(item => item.str).join(' ') + '\\n\\n';\r\n                }\r\n                \r\n                return {\r\n                    text: fullText,\r\n                    numPages: pdf.numPages\r\n                };\r\n            } catch (error) {\r\n                throw new Error('Failed to read PDF file: ' + error.message);\r\n            }\r\n        }\r\n        \r\n        \/**\r\n         * Read text from text-based files\r\n         *\/\r\n        function readTextFile(file) {\r\n            return new Promise((resolve, reject) => {\r\n                const reader = new FileReader();\r\n                reader.onload = (e) => resolve(e.target.result);\r\n                reader.onerror = (e) => reject(new Error('Failed to read file'));\r\n                reader.readAsText(file);\r\n            });\r\n        }\r\n        \r\n        \/**\r\n         * Process uploaded file and extract text content\r\n         *\/\r\n        async function processFile(file) {\r\n            try {\r\n                showStatus('Processing file...', 'loading');\r\n                \r\n                const extension = getFileExtension(file.name);\r\n                currentFileExtension = extension; \/\/ Store extension for validation\r\n                \r\n                \/\/ Validate file type\r\n                if (!SUPPORTED_TYPES.includes(extension)) {\r\n                    showStatus(`Error: Unsupported file type (${extension})`, 'error');\r\n                    return;\r\n                }\r\n                \r\n                \/\/ Store filename without extension\r\n                currentFileName = file.name.replace(\/\\.[^\/.]+$\/, \"\");\r\n                \r\n                \/\/ Display file information\r\n                fileNameDisplay.textContent = file.name;\r\n                fileTypeDisplay.textContent = extension.toUpperCase();\r\n                fileSizeDisplay.textContent = formatFileSize(file.size);\r\n                \r\n                let extractedText = '';\r\n                let numPages = 0;\r\n                \r\n                \/\/ Process different file types\r\n                if (extension === 'pdf') {\r\n                    const result = await extractPdfText(file);\r\n                    extractedText = result.text;\r\n                    numPages = result.numPages;\r\n                } \r\n                else if (extension === 'docx') {\r\n                    extractedText = await extractDocxText(file);\r\n                } \r\n                else if (extension === 'pptx') {\r\n                    extractedText = await extractPptxText(file);\r\n                } \r\n                else {\r\n                    \/\/ Text-based files\r\n                    extractedText = await readTextFile(file);\r\n                }\r\n                \r\n                \/\/ Store content\r\n                fileContent = extractedText.substring(0, 1000000);\r\n                \r\n                \/\/ Calculate word count\r\n                const words = fileContent.trim().split(\/\\s+\/).filter(word => word.length > 0);\r\n                currentWordCount = words.length; \/\/ Store word count for validation\r\n                \r\n                \/\/ Calculate character count\r\n                charCountDisplay.textContent = fileContent.length.toLocaleString();\r\n                \r\n                \/\/ Hide rows with N\/A or - values\r\n                updateFileInfoVisibility();\r\n                \r\n                \/\/ Determine how many bots are needed and split content accordingly\r\n                const numBotsNeeded = determineBotsNeeded(fileContent.length);\r\n                contentParts = splitContentForBots(fileContent, numBotsNeeded);\r\n                \r\n                \/\/ Update UI and show file info\r\n                fileInfo.classList.remove('hidden');\r\n                \r\n                \/\/ Enable keywords generation button\r\n                generateKeywordsBtn.disabled = false;\r\n                showStatus('File processed successfully!', 'success');\r\n                \r\n            } catch (error) {\r\n                showStatus('Error processing file: ' + error.message, 'error');\r\n            }\r\n        }\r\n                \r\n        \/\/ =====================================\r\n        \/\/ Multi-Bot Content Processing Functions\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Determine the number of bots needed based on content length\r\n         * With sliding window with overlap\r\n         *\/\r\n        function determineBotsNeeded(contentLength) {\r\n            if (contentLength <= COZE_CONFIG.CHUNK_SIZE) return 1;\r\n            \r\n            const step = Math.floor(COZE_CONFIG.CHUNK_SIZE * (1 - COZE_CONFIG.OVERLAP_RATIO));\r\n            return Math.min(\r\n                Math.ceil((contentLength - COZE_CONFIG.CHUNK_SIZE) \/ step) + 1,\r\n                COZE_CONFIG.BOT_IDS.length\r\n            );\r\n        }\r\n\r\n        \/**\r\n         * Split content into parts for multiple bots\r\n         *\/\r\n        function splitContentForBots(content, numBots) {\r\n            const parts = [];\r\n            const step = Math.floor(COZE_CONFIG.CHUNK_SIZE * (1 - COZE_CONFIG.OVERLAP_RATIO));\r\n            \r\n            for (let i = 0; i < numBots; i++) {\r\n                const start = i * step;\r\n                const end = start + COZE_CONFIG.CHUNK_SIZE;\r\n                if (start >= content.length) break;\r\n                \r\n                parts.push(content.substring(start, Math.min(end, content.length)));\r\n            }\r\n            \r\n            return parts;\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ Enhanced Coze API Functions with Retry Logic\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Send request to a specific Coze bot with retry logic\r\n         *\/\r\n        async function sendToCozeBot(content, botId, partIndex, retryCount = 0) {\r\n            const prompt = 'Please generate 5 keywords based on the following content: ' + content + '.';\r\n            \r\n            const payload = {\r\n                bot_id: botId,\r\n                user: COZE_CONFIG.USER_ID,\r\n                query: prompt,\r\n                stream: false\r\n            };\r\n\r\n            try {\r\n                const performCozeCall = async () => {\r\n                    const response = await fetch(COZE_CONFIG.API_URL, {\r\n                        method: 'POST',\r\n                        headers: {\r\n                            'Content-Type': 'application\/json',\r\n                            'Authorization': 'Bearer ' + COZE_API_KEY,\r\n                            'Accept': '*\/*'\r\n                        },\r\n                        body: JSON.stringify(payload)\r\n                    });\r\n\r\n                    if (!response.ok) {\r\n                        throw new Error(`HTTP ${response.status}: ${await response.text()}`);\r\n                    }\r\n\r\n                    return await response.json();\r\n                };\r\n\r\n                const data = await retryWithKeyRefresh(performCozeCall);\r\n                \r\n                \/\/ Enhanced response validation\r\n                if (!data || !data.messages || !Array.isArray(data.messages)) {\r\n                    throw new Error('Invalid response structure: missing messages array');\r\n                }\r\n\r\n                \/\/ Parse the response to extract keywords markdown\r\n                const answerMsg = data.messages.find(msg => msg && msg.type === 'answer');\r\n                if (!answerMsg || !answerMsg.content) {\r\n                    throw new Error('No valid answer message found in response');\r\n                }\r\n\r\n                let content_text = answerMsg.content;\r\n                \r\n                \/\/ Extract markdown content from the response\r\n                const markdownMatch = content_text.match(\/```markdown\\n([\\s\\S]*?)\\n```\/);\r\n                if (markdownMatch) {\r\n                    return markdownMatch[1];\r\n                } else {\r\n                    return content_text;\r\n                }\r\n\r\n            } catch (error) {\r\n                \/\/ Retry logic\r\n                if (retryCount < COZE_CONFIG.MAX_RETRIES) {\r\n                    await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1))); \/\/ Progressive delay\r\n                    return await sendToCozeBot(content, botId, partIndex, retryCount + 1);\r\n                } else {\r\n                    return null; \/\/ Return null for failed bots\r\n                }\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Enhanced duplicate keyword removal with sequence preservation\r\n         *\/\r\n        function removeDuplicateKeywords(results) {\r\n            \/\/ Filter out null results from failed bots\r\n            const validResults = results.filter(result => result !== null);\r\n            \r\n            if (validResults.length === 0) {\r\n                throw new Error('No valid responses received from any bot');\r\n            }\r\n\r\n            const sequencedKeywords = [];\r\n            const mainKeywordsMap = new Map();\r\n            \r\n            \/\/ Process each bot's response in sequence order\r\n            for (let botIndex = 0; botIndex < validResults.length; botIndex++) {\r\n                const content = validResults[botIndex];\r\n                const lines = content.split('\\n');\r\n                let currentMain = null;\r\n                let currentSub = null;\r\n\r\n                for (let i = 0; i < lines.length; i++) {\r\n                    let line = lines[i].trim();\r\n                    \r\n                    \/\/ Clean formatting\r\n                    line = line.replace(\/\\**\/g, '').replace(\/\\*\/g, '');\r\n                    \r\n                    \/\/ Skip irrelevant lines\r\n                    if (line === '---' || \r\n                        line.includes(\"I can't assist with\") || \r\n                        line.includes(\"I can't assist you with\") || \r\n                        line.includes(\"I can't help with that request\") || \r\n                        line.includes(\"I am unable to assist you with\")) {\r\n                        continue;\r\n                    }\r\n                    \r\n                    \/\/ Process main keywords (##)\r\n                    if (line.startsWith('## ')) {\r\n                        const mainText = line.substring(3).trim();\r\n                        const normalizedMain = mainText.toLowerCase();\r\n                        \r\n                        if (!mainKeywordsMap.has(normalizedMain)) {\r\n                            const mainEntry = {\r\n                                original: mainText,\r\n                                subKeywords: new Map(),\r\n                                botSequence: botIndex,\r\n                                order: sequencedKeywords.length\r\n                            };\r\n                            mainKeywordsMap.set(normalizedMain, mainEntry);\r\n                            sequencedKeywords.push(normalizedMain);\r\n                        } else {\r\n                            \/\/ Update to use higher sequence bot if found in later bot\r\n                            const existing = mainKeywordsMap.get(normalizedMain);\r\n                            if (botIndex > existing.botSequence) {\r\n                                existing.original = mainText;\r\n                                existing.botSequence = botIndex;\r\n                            }\r\n                        }\r\n                        currentMain = normalizedMain;\r\n                        currentSub = null;\r\n                    }\r\n                    \/\/ Process sub-keywords (###)\r\n                    else if (line.startsWith('### ') && currentMain) {\r\n                        const subText = line.substring(4).trim();\r\n                        const normalizedSub = subText.toLowerCase();\r\n                        const mainEntry = mainKeywordsMap.get(currentMain);\r\n                        \r\n                        if (!mainEntry.subKeywords.has(normalizedSub)) {\r\n                            mainEntry.subKeywords.set(normalizedSub, {\r\n                                original: subText,\r\n                                definitions: [],\r\n                                botSequence: botIndex\r\n                            });\r\n                        } else {\r\n                            \/\/ Update to use higher sequence bot\r\n                            const existingSub = mainEntry.subKeywords.get(normalizedSub);\r\n                            if (botIndex > existingSub.botSequence) {\r\n                                existingSub.original = subText;\r\n                                existingSub.botSequence = botIndex;\r\n                            }\r\n                        }\r\n                        currentSub = normalizedSub;\r\n                    }\r\n                    \/\/ Process definitions (####)\r\n                    else if (line.startsWith('#### ') && currentMain && currentSub) {\r\n                        const definitionText = line.substring(5).trim();\r\n                        const mainEntry = mainKeywordsMap.get(currentMain);\r\n                        const subEntry = mainEntry.subKeywords.get(currentSub);\r\n                        \r\n                        if (subEntry && subEntry.definitions.length === 0) {\r\n                            \/\/ Only add the first definition\r\n                            subEntry.definitions.push(definitionText);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n\r\n            \/\/ Build final markdown structure preserving sequence order\r\n            const result = [];\r\n            result.push('# Keywords and Definitions');\r\n            \r\n            \/\/ Sort by original order\r\n            const sortedMainKeywords = sequencedKeywords\r\n                .filter(key => mainKeywordsMap.has(key))\r\n                .sort((a, b) => mainKeywordsMap.get(a).order - mainKeywordsMap.get(b).order);\r\n            \r\n            for (const normalizedMain of sortedMainKeywords) {\r\n                const mainEntry = mainKeywordsMap.get(normalizedMain);\r\n                result.push(`## ${mainEntry.original}`);\r\n                \r\n                \/\/ Sort sub-keywords by bot sequence to maintain order\r\n                const sortedSubKeywords = Array.from(mainEntry.subKeywords.entries())\r\n                    .sort((a, b) => a[1].botSequence - b[1].botSequence);\r\n                \r\n                for (const [normalizedSub, subEntry] of sortedSubKeywords) {\r\n                    result.push(`### ${subEntry.original}`);\r\n                    \r\n                    \/\/ Use the first definition only\r\n                    if (subEntry.definitions.length > 0) {\r\n                        result.push(`#### ${subEntry.definitions[0]}`);\r\n                    }\r\n                }\r\n            }\r\n            \r\n            return result.join('\\n');\r\n        }\r\n\r\n        \/**\r\n         * Enhanced keyword generation with robust error handling\r\n         *\/\r\n        async function generateKeywordsFromCoze() {\r\n            try {\r\n                \/\/ Validate word count before proceeding\r\n                if (currentWordCount === 0) {\r\n                    if (currentFileExtension === 'pdf') {\r\n                        showStatus('No extractable text found in your PDF file. The PDF may be image-based or scanned. Please consider using an OCR tool to extract text from the file first, then try again.', 'error');\r\n                    } else {\r\n                        showStatus('No extractable text found in your file. Please upload a file with readable text content and try again.', 'error');\r\n                    }\r\n                    return;\r\n                }\r\n                \r\n                showStatus('Generating keywords with multiple bots...', 'loading');\r\n                \r\n                if (!contentParts || contentParts.length === 0) {\r\n                    throw new Error('No content parts available for processing');\r\n                }\r\n                \r\n                const numBots = contentParts.length;\r\n                \r\n                \/\/ Create promises for all bots that will be used\r\n                const promises = [];\r\n                for (let i = 0; i < numBots; i++) {\r\n                    const botId = COZE_CONFIG.BOT_IDS[i];\r\n                    const contentPart = contentParts[i];\r\n                    promises.push(sendToCozeBot(contentPart, botId, i));\r\n                }\r\n                \r\n                \/\/ Wait for all responses (including failed ones)\r\n                const results = await Promise.allSettled(promises);\r\n                \r\n                \/\/ Extract successful results and log failures\r\n                const processedResults = [];\r\n                let successCount = 0;\r\n                let failureCount = 0;\r\n                \r\n                for (let i = 0; i < results.length; i++) {\r\n                    if (results[i].status === 'fulfilled' && results[i].value !== null) {\r\n                        processedResults.push(results[i].value);\r\n                        successCount++;\r\n                    } else {\r\n                        processedResults.push(null);\r\n                        failureCount++;\r\n                    }\r\n                }\r\n                \r\n                if (successCount === 0) {\r\n                    throw new Error('All bots failed to process content');\r\n                }\r\n                \r\n                \/\/ Enhanced cleaning and duplicate removal with sequence preservation\r\n                extractedKeywords = removeDuplicateKeywords(processedResults);\r\n                \r\n                \/\/ Display the keywords in the editor\r\n                markdownEditor.value = extractedKeywords;\r\n                keywordsSection.classList.remove('hidden');\r\n\r\n            } catch (error) {\r\n                showStatus('Error generating keywords: ' + error.message, 'error');\r\n            }\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ Mindmap\r\n        \/\/ =====================================\r\n\r\n        \/**\r\n         * Generate mindmap with direct markmap integration\r\n         *\/\r\n        function generateMindmap() {\r\n            try {\r\n                showStatus('Generating mindmap...', 'loading');\r\n                \r\n                const markdownContent = markdownEditor.value;\r\n                if (!markdownContent.trim()) {\r\n                    showStatus('Please provide keywords content first.', 'error');\r\n                    return;\r\n                }\r\n\r\n                \/\/ Use the filename as title (or fallback to \"Keywords\" if empty)\r\n                const title = currentFileName || 'Keywords';\r\n                \r\n                \/\/ Parse the three-level structure\r\n                const lines = markdownContent.split('\\n');\r\n                let mindmapMarkdown = `---\\ntitle: ${title}\\nmarkmap:\\n  colorFreezeLevel: 2\\n  initialExpandLevel: 3\\n  maxWidth: 600\\n---\\n\\n`;\r\n                \r\n                for (const line of lines) {\r\n                    const trimmed = line.trim();\r\n                    if (trimmed.startsWith('## ')) {\r\n                        \/\/ Main keyword - add ** for bold\r\n                        const keyword = trimmed.substring(3).trim();\r\n                        mindmapMarkdown += `## **${keyword}**\\n`;\r\n                    } else if (trimmed.startsWith('### ')) {\r\n                        \/\/ Sub-keyword - add ** for bold\r\n                        const subKeyword = trimmed.substring(4).trim();\r\n                        mindmapMarkdown += `### ${subKeyword}\\n`;\r\n                    } else if (trimmed.startsWith('#### ')) {\r\n                        \/\/ Definition - leave as is\r\n                        mindmapMarkdown += `- ${trimmed.substring(5)}\\n`;\r\n                    }\r\n                }\r\n\r\n                \/\/ Render the mindmap\r\n                renderMindmapDirect(mindmapMarkdown);\r\n                mindmapSection.classList.remove('hidden');\r\n                showStatus('Mindmap generated successfully!', 'success');\r\n\r\n            } catch (error) {\r\n                showStatus('Error generating mindmap: ' + error.message, 'error');\r\n            }\r\n        }\r\n\r\n        \/**\r\n         * Render mindmap directly in the DOM using markmap-autoloader (WordPress-compatible)\r\n         *\/\r\n        function renderMindmapDirect(markdownContent) {\r\n            try {\r\n                \/\/ Clear existing content\r\n                mindmapContainer.innerHTML = '';\r\n                \r\n                \/\/ Create the markmap structure directly in the DOM\r\n                const markmapDiv = document.createElement('div');\r\n                markmapDiv.className = 'markmap';\r\n                \r\n                \/\/ Create script template element\r\n                const scriptTemplate = document.createElement('script');\r\n                scriptTemplate.type = 'text\/template';\r\n                scriptTemplate.textContent = markdownContent;\r\n                \r\n                \/\/ Append to the markmap div\r\n                markmapDiv.appendChild(scriptTemplate);\r\n                \r\n                \/\/ Add to container\r\n                mindmapContainer.appendChild(markmapDiv);\r\n                \r\n                \/\/ Force markmap-autoloader to process the new content\r\n                \/\/ This is WordPress-compatible because it doesn't use iframe\r\n                if (window.markmap && window.markmap.autoloader) {\r\n                    window.markmap.autoloader.ready();\r\n                } else {\r\n                    \/\/ Fallback: trigger markmap processing after a brief delay\r\n                    setTimeout(() => {\r\n                        \/\/ Force re-scan for markmap elements\r\n                        const event = new Event('DOMContentLoaded');\r\n                        document.dispatchEvent(event);\r\n                    }, 100);\r\n                }\r\n                \r\n            } catch (error) {\r\n                throw error;\r\n            }\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ UI Helper Functions\r\n        \/\/ =====================================\r\n        \r\n        \/**\r\n         * Show status message\r\n         *\/\r\n        function showStatus(message, type) {\r\n            statusDiv.innerHTML = '<div class=\"status ' + type + '\">' + message + '<\/div>';\r\n        }\r\n\r\n        \/\/ =====================================\r\n        \/\/ Event Listeners\r\n        \/\/ =====================================\r\n        \r\n        \/\/ File drag and drop functionality\r\n        dropArea.addEventListener('click', function() { \r\n            fileInput.click(); \r\n        });\r\n        \r\n        ['dragover', 'dragenter'].forEach(function(eventType) {\r\n            dropArea.addEventListener(eventType, function(e) {\r\n                e.preventDefault();\r\n                dropArea.classList.add('active');\r\n            });\r\n        });\r\n        \r\n        ['dragleave', 'dragend'].forEach(function(eventType) {\r\n            dropArea.addEventListener(eventType, function() {\r\n                dropArea.classList.remove('active');\r\n            });\r\n        });\r\n        \r\n        dropArea.addEventListener('drop', function(e) {\r\n            e.preventDefault();\r\n            e.stopPropagation();\r\n            dropArea.classList.remove('active');\r\n            \r\n            if (e.dataTransfer.files.length > 0) {\r\n                const file = e.dataTransfer.files[0];\r\n                processFile(file);\r\n            }\r\n        });\r\n\r\n        \/\/ File input change handler\r\n        fileInput.addEventListener('change', function(e) {\r\n            const file = e.target.files[0];\r\n            if (file) {\r\n                processFile(file);\r\n            }\r\n        });\r\n\r\n        \/\/ Generate keywords button\r\n        generateKeywordsBtn.addEventListener('click', function() {\r\n            if (contentParts && contentParts.length > 0) {\r\n                generateKeywordsFromCoze();\r\n            } else {\r\n                showStatus('Please upload a document first.', 'error');\r\n            }\r\n        });\r\n\r\n        \/\/ Generate mindmap button\r\n        generateMindmapBtn.addEventListener('click', generateMindmap);\r\n\r\n        \/\/ Initialize the application\r\n        window.addEventListener('DOMContentLoaded', async () => {\r\n            \/\/ Fetch API keys from backend\r\n            try {\r\n                console.log('Initializing API keys from backend...');\r\n                const { cozeKey } = await initializeApiKeys();\r\n                COZE_API_KEY_RUNTIME = cozeKey;\r\n                console.log('\u2713 Coze API key initialized successfully');\r\n            } catch (error) {\r\n                console.error('Failed to initialize API keys:', error);\r\n                alert('Authorisation was not granted by the server. This may be due to a network issue. Please check your connection and try again.');\r\n            }\r\n        });\r\n\r\n        \/\/ Clear API keys when page is closed\r\n        window.addEventListener('beforeunload', () => {\r\n            clearApiKeys();\r\n        });\r\n    <\/script>\r\n<\/body>\r\n<\/html>\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Mind Map Creator Interactive Mind Map Designer Instantly visualize document structure as a colorful, clickable mind map. Expand or collapse nodes to explore relationships and strengthen concept retention. Step 1: Upload Document Drag &#038; drop document here or click to select Supported formats: PDF, DOCX, PPTX, TXT, HTML, MD, XML, JSON, CSV File Successfully Loaded...<\/p>\n","protected":false},"author":748,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"redux-templates_canvas","meta":{"_expiration-date-status":"","_expiration-date":0,"_expiration-date-type":"","_expiration-date-categories":[],"_expiration-date-options":[]},"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v17.3 (Yoast SEO v21.2) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Agent 5 Mind Map Creator - Hong Kong Metropolitan University<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Agent 5 Mind Map Creator\" \/>\n<meta property=\"og:description\" content=\"Mind Map Creator Interactive Mind Map Designer Instantly visualize document structure as a colorful, clickable mind map. Expand or collapse nodes to\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/\" \/>\n<meta property=\"og:site_name\" content=\"Hong Kong Metropolitan University\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-03T09:05:31+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Agent 5 Mind Map Creator - Hong Kong Metropolitan University","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/","og_locale":"en_US","og_type":"article","og_title":"Agent 5 Mind Map Creator","og_description":"Mind Map Creator Interactive Mind Map Designer Instantly visualize document structure as a colorful, clickable mind map. Expand or collapse nodes to","og_url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/","og_site_name":"Hong Kong Metropolitan University","article_modified_time":"2025-11-03T09:05:31+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/","url":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/","name":"Agent 5 Mind Map Creator - Hong Kong Metropolitan University","isPartOf":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website"},"datePublished":"2025-06-25T04:08:53+00:00","dateModified":"2025-11-03T09:05:31+00:00","breadcrumb":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/agent5mindmapcreator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Open Educational Tools","item":"\/oetools\/"},{"@type":"ListItem","position":2,"name":"Agent 5 Mind Map Creator"}]},{"@type":"WebSite","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website","url":"https:\/\/www.hkmu.edu.hk\/oetools\/","name":"Hong Kong Metropolitan University","description":"Open Educational Tools - Hong Kong Metropolitan University","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.hkmu.edu.hk\/oetools\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/27064"}],"collection":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/users\/748"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/comments?post=27064"}],"version-history":[{"count":125,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/27064\/revisions"}],"predecessor-version":[{"id":30915,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/27064\/revisions\/30915"}],"wp:attachment":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/media?parent=27064"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}