WME Workflow Engine

Xây dựng và chạy các chuỗi tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV và thực thi các hành động như click, nhập liệu trên các đối tượng.

// ==UserScript==
// @name         WME Workflow Engine
// @namespace    https://greasyfork.org/
// @version      2.0.2
// @description  Xây dựng và chạy các chuỗi tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV và thực thi các hành động như click, nhập liệu trên các đối tượng.
// @author       Minh Tan
// @match        https://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @exclude      https://www.waze.com/*user/editor*
// @grant        none
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// ==/UserScript==
/* global W, WazeWrap, $, XLSX */
(function () {
    'use strict';
    let permalinks = [];
    let currentIndex = -1;
    let allWorkflows = {};
    let isLooping = false;
    const DELAY_BETWEEN_LOOPS = 300;
    const STORAGE_KEY = 'wme_custom_workflows';
    const defaultWorkflows = {
        "street_city_update": {
            name: "Cập nhật T.Phố (Street)",
            steps: [
                { type: 'click', selector: '.w-icon.w-icon-pencil-fill.edit-button', desc: "Click nút Edit Address" },
                { type: 'delay', delay: 100, desc: "Chờ UI load" },
                { type: 'input', selector: '#segment-edit-general > div:nth-child(2) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete', shadowSelector: '#text-input', value: '{{value}}', desc: "Nhập tên TP mới" },
                { type: 'delay', delay: 100, desc: "Chờ gợi ý hiện ra" },
                { type: 'click', selector: '#segment-edit-general > div:nth-child(2) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete', shadowSelector: 'wz-menu-item:nth-child(2)', desc: "Chọn gợi ý đầu tiên" },
                { type: 'delay', delay: 100, desc: "Chờ xử lý" },
                { type: 'click', selector: '#segment-edit-general .save-button', desc: "Click nút Save" }
            ]
        },
        "place_city_update": {
            name: "Cập nhật T.Phố (Place)",
            steps: [
                { type: 'click', selector: '#venue-edit-general .w-icon-pencil-fill.edit-button', desc: "Click nút Edit Address" },
                { type: 'delay', delay: 100, desc: "Chờ UI load" },
                { type: 'input', selector: '#venue-edit-general > div:nth-child(1) > div > div.address-edit-view > wz-card > form > div:nth-child(4) > wz-autocomplete', shadowSelector: '#text-input', value: '{{value}}', desc: "Nhập tên TP mới" },
                { type: 'delay', delay: 100, desc: "Chờ gợi ý hiện ra" },
                { type: 'click', selector: '#venue-edit-general > div:nth-child(1) > div > div.address-edit-view > wz-card > form > div:nth-child(4) > wz-autocomplete', shadowSelector: 'wz-menu-item:nth-child(2)', desc: "Chọn gợi ý đầu tiên" },
                { type: 'delay', delay: 100, desc: "Chờ xử lý" },
                { type: 'click', selector: '#venue-edit-general .save-button', desc: "Click nút Save" }
            ]
        }
    };
    function bootstrap() {
        if (W && W.map && W.model && W.loginManager.user && $ && WazeWrap.Ready) {
            init();
        } else {
            setTimeout(bootstrap, 500);
        }
    }
    function init() {
        console.log("WME Workflow Engine: Initialized");
        loadWorkflows();
        createUI();
        createWorkflowEditorModal();
        populateWorkflowSelector();
        updateUIState();
        registerHotkeys();
    }
    /**
     * Tìm một phần tử, hỗ trợ tìm kiếm bên trong Shadow DOM.
     * @param {string} selector - CSS selector cho phần tử chính.
     * @param {string} [shadowSelector] - CSS selector cho phần tử bên trong shadow DOM.
     * @returns {Promise<Element|null>}
     */
    async function findElement(selector, shadowSelector = '') {
        try {
            const baseElement = await waitForElement(selector);
            if (!shadowSelector) {
                return baseElement;
            }
            if (baseElement && baseElement.shadowRoot) {
                await delay(50);
                const shadowElement = baseElement.shadowRoot.querySelector(shadowSelector);
                if (!shadowElement) {
                    log(`Lỗi: Không tìm thấy phần tử con với selector "${shadowSelector}" trong shadow DOM của "${selector}".`, 'error');
                }
                return shadowElement;
            }
            log(`Lỗi: Không tìm thấy shadow root trên phần tử "${selector}".`, 'error');
            return null;
        } catch (error) {
            log(`Lỗi khi tìm phần tử "${selector}": ${error.message}`, 'error');
            return null;
        }
    }
    /**
     * Thực thi một bước (step) của workflow.
     * @param {object} step - Đối tượng step.
     * @param {string} variableValue - Giá trị để thay thế cho placeholder {{value}}.
     */
    async function executeStep(step, variableValue) {
        try {
            switch (step.type) {
                case 'click':
                    {
                        const element = await findElement(step.selector, step.shadowSelector);
                        if (element) {
                            element.click();
                        } else throw new Error("Phần tử không tồn tại để click.");
                        break;
                    }
                case 'input':
                    {
                        const element = await findElement(step.selector, step.shadowSelector);
                        if (element) {
                            const valueToInput = step.value.replace('{{value}}', variableValue);
                            element.value = valueToInput;
                            element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
                            element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
                        } else throw new Error("Phần tử không tồn tại để nhập liệu.");
                        break;
                    }
                case 'delay':
                    {
                        await delay(step.delay);
                        break;
                    }
                case 'log':
                    {
                        break;
                    }
                default:
                    log(`Loại step không xác định: ${step.type}`, 'error');
            }
        } catch (err) {
            log(`❌ Lỗi khi thực thi step (${step.type}): ${err.message}`, 'error');
            throw err;
        }
    }
    /**
     * Chạy toàn bộ workflow đã chọn.
     */
    async function runSelectedWorkflow() {
        const workflowId = document.getElementById('workflow_select').value;
        const variableValue = document.getElementById('workflow_variable_input').value;

        if (!workflowId || !allWorkflows[workflowId]) {
            alert("Vui lòng chọn một workflow hợp lệ!");
            throw new Error("Workflow không hợp lệ.");
        }

        const workflow = allWorkflows[workflowId];

        try {
            for (const step of workflow.steps) {
                if (!isLooping && step.type !== 'log') { // Cho phép dừng giữa chừng, trừ log
                    const userWantsToStop = await new Promise(resolve => {
                        // Nếu vòng lặp không hoạt động, không cần kiểm tra
                        if (!isLooping) resolve(false);
                        else resolve(false); // Bỏ qua kiểm tra trong bản cập nhật này để đơn giản hóa
                    });
                    if (userWantsToStop) {
                        throw new Error("Workflow bị dừng bởi người dùng.");
                    }
                }
                await executeStep(step, variableValue);
            }
        } catch (error) {
            log(`--- ❌ Workflow "${workflow.name}" đã dừng: ${error.message} ---`, 'error');
            throw error;
        }
    }



    async function toggleWorkflowLoop() {
        isLooping = !isLooping;
        updateUIState();

        if (isLooping) {
            log("--- Bắt đầu vòng lặp tự động ---", 'special');
            await executeLoop();
        } else {
            log("Đã yêu cầu dừng vòng lặp. Sẽ dừng sau khi hoàn thành mục hiện tại.", 'warn');
        }
    }

    async function executeLoop() {
        while (isLooping && currentIndex < permalinks.length) {
            updateUIState(); // Cập nhật UI để hiển thị đúng index

            try {
                // Điều hướng và chọn đối tượng
                await processCurrentLink();
                await delay(1500); // Chờ cho WME ổn định sau khi điều hướng

                // Chạy workflow
                await runSelectedWorkflow();

            } catch (error) {
                log(`Lỗi ở mục ${currentIndex + 1}, bỏ qua và tiếp tục. Lỗi: ${error.message}`, 'error');
            }

            // Kiểm tra lần nữa nếu người dùng đã nhấn dừng trong lúc workflow chạy
            if (!isLooping) break;

            // Chuyển sang mục tiếp theo
            if (currentIndex < permalinks.length - 1) {
                currentIndex++;
                log(`Đang chờ ${DELAY_BETWEEN_LOOPS / 1000}s trước khi đến mục tiếp theo...`, 'info');
                await delay(DELAY_BETWEEN_LOOPS);
            } else {
                log("Đã đến mục cuối cùng của danh sách.", 'info');
                break; // Thoát vòng lặp khi đã ở mục cuối
            }
        }

        isLooping = false;
        if (currentIndex >= permalinks.length -1) {
            log("--- ✅ Hoàn thành vòng lặp tự động! ---", 'special');
        } else {
            log("--- Vòng lặp tự động đã dừng. ---", 'warn');
        }
        updateUIState(); // Cập nhật UI lần cuối để mở lại các nút
    }

    function handleFile(e) {
        permalinks = [];
        currentIndex = -1;
        const file = e.target.files[0];
        if (!file) {
            updateUIState();
            return;
        }
        const urlColumnInput = document.getElementById('url_column').value.toUpperCase();
        const urlColumnIndex = urlColumnInput.charCodeAt(0) - 'A'.charCodeAt(0);
        if (urlColumnIndex < 0 || urlColumnIndex > 25) {
            log(`Lỗi: Cột "${urlColumnInput}" không hợp lệ. Vui lòng nhập A-Z.`, 'error');
            return;
        }
        const reader = new FileReader();
        reader.onload = (e) => {
            try {
                const data = new Uint8Array(e.target.result);
                const workbook = XLSX.read(data, { type: 'array' });
                const firstSheetName = workbook.SheetNames[0];
                const worksheet = workbook.Sheets[firstSheetName];
                const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
                permalinks = json
                    .slice(1) // Bỏ qua hàng tiêu đề
                    .map(row => row[urlColumnIndex])
                    .filter(pl => pl && typeof pl === 'string' && (pl.includes('waze.com/editor') || pl.includes('waze.com/ul'))); // Chỉ lấy permalinks hợp lệ
                if (permalinks.length > 0) {
                    currentIndex = 0;
                    processCurrentLink();
                } else {
                    log(`Không tìm thấy URL hợp lệ trong cột ${urlColumnInput} của file.`, 'warn');
                }
                updateUIState();
            } catch (err) {
                log(`Lỗi khi đọc file: ${err.message}`, 'error');
                updateUIState();
            }
        };
        reader.readAsArrayBuffer(file);
    }
    function saveWorkflows() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(allWorkflows));
            log("Đã lưu các workflows.");
        } catch (e) {
            log("Lỗi khi lưu workflows vào localStorage.", 'error');
        }
    }
    function loadWorkflows() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                allWorkflows = JSON.parse(saved);
            } else {
                allWorkflows = { ...defaultWorkflows };
                log("Đã tải các workflows mặc định. Các thay đổi sẽ được lưu lại.");
            }
        } catch (e) {
            log("Lỗi khi tải workflows từ localStorage, sử dụng các preset mặc định.", 'error');
            allWorkflows = { ...defaultWorkflows };
        }
    }
    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
    function waitForElement(selector, timeout = 7000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let elapsedTime = 0;
            const interval = setInterval(() => {
                const element = document.querySelector(selector);
                // Check if element exists and is visible (offsetParent is not null)
                if (element && element.offsetParent !== null) {
                    clearInterval(interval);
                    resolve(element);
                }
                elapsedTime += intervalTime;
                if (elapsedTime >= timeout) {
                    clearInterval(interval);
                    reject(new Error(`Element "${selector}" not found or not visible after ${timeout}ms`));
                }
            }, intervalTime);
        });
    }
    function log(message, type = 'normal') {
        const logBox = document.getElementById('log_info');
        if (logBox) {
            const colorMap = {
                error: '#c0392b',
                success: '#27ae60',
                warn: '#e67e22',
                info: '#2980b9',
                special: '#8e44ad',
                normal: 'inherit'
            };
            const div = document.createElement('div');
            div.style.color = colorMap[type];
            div.innerHTML = `[${new Date().toLocaleTimeString()}] ${message.replace(/</g, "<").replace(/>/g, ">")}`; // Sanitize HTML
            logBox.prepend(div); // Add to top
            // Limit log box to 50 entries
            while (logBox.children.length > 50) {
                logBox.removeChild(logBox.lastChild);
            }
        }
        console.log(`[WME Workflow] ${message}`);
    }
    function createUI() {
        const panel = document.createElement('div');
        panel.id = 'workflow-engine-panel';
        panel.style.cssText = `
    position: fixed; top: 80px; left: 15px; background: rgba(255, 255, 255, 0.85); border: 1px solid #ccc;
    padding: 0; z-index: 1001; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; width: 420px;
    `;
        panel.innerHTML = `
    <h3 id="navigator-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 10px 15px; cursor: grab; border-bottom: 1px solid #eee;">
        <span>WME Workflow Engine</span>
        <button id="toggle_panel_btn" title="Thu gọn Panel">▲</button>
    </h3>
    <div id="wwe-panel-content" style="padding: 15px;">
        <!-- Section 1: Điều khiển chính -->
        <h4 style="margin-top: 0; margin-bottom: 12px;">Điều khiển chính</h4>
        <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
            <button id="prev_btn" class="nav-btn" title="Đối tượng trước (Mũi tên trái)" disabled>◀</button>
            <div style="display: flex; align-items: center; flex-grow: 1;">
                <input type="number" id="nav_index_input" min="1" style="width: 100%; text-align: center;" disabled>
                <span id="nav_total_count" style="margin-left: 5px;">/ N/A</span>
            </div>
            <button id="next_btn" class="nav-btn" title="Đối tượng tiếp theo (Mũi tên phải)" disabled>▶</button>
        </div>
         
         <div class="wwe-form-group">
            <label for="workflow_select">Chọn Workflow:</label>
            <select id="workflow_select"></select>
         </div>
         <div class="wwe-form-group">
            <label for="workflow_variable_input">Giá trị nhập (cho <code>{{value}}</code>):</label>
            <input type="text" id="workflow_variable_input" placeholder="Tên thành phố, giá trị khác..." />
         </div>
         <button id="run_workflow_btn" class="action-btn primary" style="width: 100%;" title="Chạy workflow (Mũi tên xuống)" disabled>▶️ Chạy Workflow</button>
         <button id="loop_workflow_btn" class="action-btn" style="width: 100%;" title="Tự động chạy workflow cho tất cả đối tượng" disabled>🔁 Bắt đầu Lặp</button>
        <!-- Section 2: Accordion Items -->
        <div class="accordion-container" style="margin-top: 15px;">
            <!-- Accordion: Tải dữ liệu -->
            <div class="accordion-item">
                <button class="accordion-header">Tải & Cấu hình Dữ liệu</button>
                <div class="accordion-content">
                    <div class="wwe-form-group">
                        <input type="file" id="excel_file" accept=".xlsx, .xls, .csv"/>
                    </div>
                    <div class="wwe-form-group">
                         <label for="url_column">Cột URL:</label>
                         <input type="text" id="url_column" value="F" size="5" style="text-transform: uppercase; text-align: center;">
                    </div>
                    <button id="reselect_btn" class="action-btn secondary" style="width: 100%; margin-top: 10px;" title="Tải lại đối tượng hiện tại (Mũi tên lên)" disabled>🔄 Tải lại & Chọn</button>
                </div>
            </div>
            <!-- Accordion: Quản lý Workflows -->
            <div class="accordion-item">
                <button class="accordion-header">Quản lý Workflows</button>
                <div class="accordion-content">
                    <div style="display: flex; gap: 10px; margin-top: 10px;">
                        <button id="edit_workflow_btn" class="action-btn" style="flex-grow: 1;">✏️ Sửa</button>
                        <button id="new_workflow_btn" class="action-btn" style="flex-grow: 1;">➕ Tạo mới</button>
                        <button id="delete_workflow_btn" class="action-btn danger" style="flex-grow: 1;">🗑️ Xóa</button>
                    </div>
                </div>
            </div>
            <!-- Accordion: Nhật ký -->
            <div class="accordion-item">
                <button class="accordion-header">Nhật ký Hoạt động</button>
                <div class="accordion-content">
                     <div id="log_info" style="font-size: 12px; height: 120px; overflow-y: auto; border: 1px solid #eee; padding: 5px; background: #f8f9fa; border-radius: 4px; margin-top: 5px;"></div>
                </div>
            </div>
        </div>
    </div>
    `;
        document.body.appendChild(panel);
        const style = document.createElement('style');
        style.innerHTML = `
    /* Global button/input styles for both panel and modal */
    #workflow-engine-panel button, #workflow-editor-modal button {
        border: 1px solid #ccc;
        background-color: #f0f0f0;
        border-radius: 4px;
        padding: 5px 10px;
        cursor: pointer;
        transition: background-color 0.2s, border-color 0.2s;
    }
    #workflow-engine-panel button:hover:not(:disabled), #workflow-editor-modal button:hover:not(:disabled) {
        background-color: #e0e0e0;
    }
    #workflow-engine-panel button:disabled, #workflow-editor-modal button:disabled {
        cursor: not-allowed;
        opacity: 0.5;
    }
    #workflow-engine-panel input[type=text], #workflow-engine-panel input[type=number], #workflow-engine-panel input[type=file], #workflow-engine-panel select,
    #workflow-editor-modal input[type=text], #workflow-editor-modal select {
        border-radius: 4px;
        border: 1px solid #ccc;
        width: 100%;
        box-sizing: border-box;
        padding: 5px; /* Added padding for better input appearance */
    }

    /* Toggle buttons styles */
    #toggle_panel_btn, #toggle_editor_panel_btn {
        background: none;
        border: none;
        cursor: pointer;
        font-size: 20px;
        line-height: 1;
        padding: 0 5px;
        color: #888;
        font-weight: bold;
    }
    #toggle_panel_btn:hover, #toggle_editor_panel_btn:hover {
        color: #000;
    }

    /* Main Panel Specific Styles */
    #workflow-engine-panel.is-collapsed #wwe-panel-content {
        display: none;
    }
    #workflow-engine-panel.is-collapsed #navigator-header {
        border-bottom: none;
    }
    #workflow-engine-panel .nav-btn { font-size: 16px; padding: 5px 12px; }
    #workflow-engine-panel .action-btn { padding: 8px 10px; font-weight: 500; }
    #workflow-engine-panel .action-btn.primary { background-color: #007bff; color: white; border-color: #007bff; }
    #workflow-engine-panel .action-btn.primary:hover:not(:disabled) { background-color: #0056b3; }
    #workflow-engine-panel .action-btn.secondary { background-color: #6c757d; color: white; border-color: #6c757d; }
    #workflow-engine-panel .action-btn.secondary:hover:not(:disabled) { background-color: #5a6268; }
    #workflow-engine-panel .action-btn.danger { background-color: #dc3545; color: white; border-color: #dc3545; }
    #workflow-engine-panel .action-btn.danger:hover:not(:disabled) { background-color: #c82333; }
    #loop_workflow_btn.looping { background-color: #ffc107; border-color: #ffc107; color: black; }
    #loop_workflow_btn.looping:hover:not(:disabled) { background-color: #e0a800; border-color: #d39e00;}

    /* UI Căn chỉnh cho các form group */
    .wwe-form-group {
        display: flex;
        flex-direction: column;
        margin-bottom: 8px; /* Added margin for spacing */
    }
    .wwe-form-group label {
        font-weight: bold;
        font-size: 13px;
        margin-bottom: 4px;
    }

    /* Accordion Styles */
    .accordion-item { border-top: 1px solid #eee; }
    .accordion-header {
        background-color: #f7f7f7; color: #444; cursor: pointer; padding: 10px;
        width: 100%; border: none; text-align: left; outline: none;
        font-size: 14px; transition: background-color 0.2s; font-weight: bold;
    }
    .accordion-header:hover { background-color: #e9e9e9; }
    .accordion-header::after { content: ' ▼'; font-size: 10px; float: right; }
    .accordion-header.active::after { content: ' ▲'; }
    .accordion-content {
        padding: 0 15px 0 15px; background-color: white;
        max-height: 0; overflow: hidden;
        transition: max-height 0.3s ease-out;
    }

    /* Workflow Editor Modal Styles */
    #workflow-editor-modal {
        display: none; /* Hidden by default */
        position: fixed; /* Stay in place */
        z-index: 1002; /* Sit on top */
        left: 0; top: 0;
        width: 100%; height: 100%;
        overflow: auto; /* Enable scroll if needed */
        background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
    }

    #workflow-editor-content {
        background-color: #fefefe;
        padding: 0; /* Changed to 0 as header/content div will have padding */
        border: 1px solid #888;
        width: 80%;
        max-width: 600px;
        border-radius: 8px;
        box-shadow: 0 5px 15px rgba(0,0,0,0.3);
        position: absolute; /* Allows top/left to be set for dragging */
        top: 50%; /* Initial centering */
        left: 50%; /* Initial centering */
        transform: translate(-50%, -50%); /* Initial centering */
    }

    #editor-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin:0;
        padding: 10px 15px;
        cursor: grab; /* Indicate draggable */
        border-bottom: 1px solid #eee;
        font-size: 18px; /* Larger title for modal header */
        font-weight: bold;
    }

    #editor-title {
        flex-grow: 1; /* Allows title to take available space */
    }

    #close-modal {
        margin-right: 10px; /* Space between close and toggle buttons */
    }

    /* Rules for collapsing the editor modal */
    #workflow-editor-content.is-collapsed #editor-panel-content {
        display: none;
    }
    #workflow-editor-content.is-collapsed #editor-header {
        border-bottom: none; /* Remove border when collapsed */
    }

    /* Workflow steps list */
    #workflow_steps_list {
        list-style: none;
        padding: 0;
        min-height: 100px;
        border: 1px dashed #ccc;
        padding: 10px;
        border-radius: 4px;
        margin-top: 10px; /* Added margin */
    }
    #workflow_steps_list li {
        position: relative;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 8px;
        border: 1px solid #ddd;
        margin-bottom: 5px;
        border-radius: 4px;
        background: #fafafa;
        cursor: grab;
    }
    #workflow_steps_list li .step-number {
        margin-right: 8px; /* Space out the number */
        font-weight: bold;
        color: #888;
    }
    #workflow_steps_list li.editing {
        background-color: #e0eafc !important;
        border-color: #007bff !important;
    }
    #workflow_steps_list li.dragging {
        opacity: 0.5;
    }
    #add-step-form {
        margin-top: 15px;
        padding-top: 15px;
        border-top: 1px solid #eee;
    }
`;
        document.head.appendChild(style);

        // Accordion functionality for main panel
        panel.querySelectorAll('.accordion-header').forEach(button => {
            button.addEventListener('click', () => {
                // Close other open accordions
                panel.querySelectorAll('.accordion-header.active').forEach(activeButton => {
                    if (activeButton !== button) {
                        activeButton.classList.remove('active');
                        activeButton.nextElementSibling.style.maxHeight = null;
                    }
                });
                // Toggle current accordion
                button.classList.toggle('active');
                const content = button.nextElementSibling;
                if (content.style.maxHeight) {
                    content.style.maxHeight = null;
                } else {
                    content.style.maxHeight = content.scrollHeight + "px";
                }
            });
        });

        // Toggle panel button for main panel
        document.getElementById('toggle_panel_btn').addEventListener('click', (e) => {
            e.stopPropagation(); // Ngăn sự kiện kéo-thả của header
            const isCollapsed = panel.classList.toggle('is-collapsed');
            e.currentTarget.innerHTML = isCollapsed ? '▼' : '▲';
            e.currentTarget.title = isCollapsed ? 'Mở rộng Panel' : 'Thu gọn Panel';
        });

        // Event listeners for main panel controls
        document.getElementById('excel_file').addEventListener('change', handleFile, false);
        document.getElementById('prev_btn').addEventListener('click', () => navigate(-1));
        document.getElementById('next_btn').addEventListener('click', () => navigate(1));
        document.getElementById('reselect_btn').addEventListener('click', processCurrentLink);
        document.getElementById('run_workflow_btn').addEventListener('click', runSelectedWorkflow);
        document.getElementById('loop_workflow_btn').addEventListener('click', toggleWorkflowLoop);
        document.getElementById('nav_index_input').addEventListener('change', (e) => {
            const targetIndex = parseInt(e.target.value, 10);
            if (!isNaN(targetIndex)) {
                navigate(0, targetIndex - 1); // targetIndex is 1-based, array index is 0-based
            }
        });
        document.getElementById('workflow_select').addEventListener('change', updateUIState);
        document.getElementById('new_workflow_btn').addEventListener('click', () => openWorkflowEditor());
        document.getElementById('edit_workflow_btn').addEventListener('click', () => {
            const id = document.getElementById('workflow_select').value;
            if (id) openWorkflowEditor(id);
        });
        document.getElementById('delete_workflow_btn').addEventListener('click', deleteSelectedWorkflow);

        // Make the main panel draggable
        makeDraggable(panel, document.getElementById('navigator-header'));
    };

    /**
     * Makes an element draggable using its handle.
     * @param {HTMLElement} elementToMove The element that will be moved.
     * @param {HTMLElement} dragHandle The element that acts as the drag handle.
     */
    function makeDraggable(elementToMove, dragHandle) {
        let offsetX, offsetY;
        let isDragging = false;

        dragHandle.onmousedown = (e) => {
            e.preventDefault();
            isDragging = true;
            dragHandle.style.cursor = 'grabbing'; // Change cursor while dragging

            // Get the element's current computed style to check its position
            const computedStyle = getComputedStyle(elementToMove);
            if (computedStyle.position === 'static') {
                elementToMove.style.position = 'absolute'; // Change to absolute if not already positioned
            }

            // If the element has a transform property (like translate for centering),
            // apply that transform to its top/left before dragging starts.
            if (computedStyle.transform && computedStyle.transform !== 'none') {
                const matrix = new DOMMatrixReadOnly(computedStyle.transform);
                // Adjust element's current top/left by its transform translate values
                elementToMove.style.left = (elementToMove.offsetLeft + matrix.m41) + 'px';
                elementToMove.style.top = (elementToMove.offsetTop + matrix.m42) + 'px';
                elementToMove.style.transform = 'none'; // Clear the transform
            }

            // Calculate the initial offset from the element's current position to the mouse click
            const rect = elementToMove.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;

            document.onmousemove = (ev) => {
                if (!isDragging) return;
                // Calculate new position based on mouse position and initial offset
                elementToMove.style.left = (ev.clientX - offsetX) + 'px';
                elementToMove.style.top = (ev.clientY - offsetY) + 'px';
            };

            document.onmouseup = () => {
                isDragging = false;
                document.onmouseup = null;
                document.onmousemove = null;
                dragHandle.style.cursor = 'grab'; // Reset cursor
            };
        };
    }


    function createWorkflowEditorModal() {
        const modal = document.createElement('div');
        modal.id = 'workflow-editor-modal';
        // Modal background and centering is removed here, workflow-editor-content handles positioning
        modal.style.cssText = `
            display: none; /* Hidden by default, shown by JS */
            position: fixed; /* Stay in place */
            z-index: 1002; /* Sit on top */
            left: 0; top: 0;
            width: 100%; height: 100%;
            overflow: auto; /* Enable scroll if needed */
            background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
        `;

        modal.innerHTML = `
            <div id="workflow-editor-content" style="background-color: #fefefe; padding: 0; border: 1px solid #888; width: 80%; max-width: 600px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
                <h3 id="editor-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 10px 15px; cursor: grab; border-bottom: 1px solid #eee;">
                    <span id="editor-title">Chỉnh sửa Workflow</span>
                    <div>
                        <span id="close-modal" style="color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; margin-right: 10px;">×</span>
                        <button id="toggle_editor_panel_btn" title="Thu gọn/Mở rộng Panel">▲</button>
                    </div>
                </h3>
                <div id="editor-panel-content" style="padding: 15px;">
                    <input type="hidden" id="editing_workflow_id">
                    <div class="wwe-form-group">
                        <label for="workflow_name_input">Tên Workflow:</label>
                        <input type="text" id="workflow_name_input" placeholder="VD: Khóa đường cấm xe máy">
                    </div>
                    <h4>Các bước (kéo thả để sắp xếp):</h4>
                    <ul id="workflow_steps_list"></ul>
                    <div id="add-step-form">
                        <h4>Thêm/Sửa bước:</h4>
                        <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 10px; align-items: center;">
                            <label>Loại hành động:</label>
                            <select id="step_type_select">
                                <option value="click">Click</option>
                                <option value="input">Input</option>
                                <option value="delay">Delay</option>
                                <option value="log">Log</option>
                            </select>
                            <label>Mô tả ngắn:</label>
                            <input type="text" id="step_desc_input" placeholder="VD: Click nút Lưu">
                            <label>Selector (JS Path):</label>
                            <input type="text" id="step_selector_input" placeholder="CSS selector, VD: .save-button">
                            <label>Shadow Selector (Nếu có):</label>
                            <input type="text" id="step_shadow_input" placeholder="VD: #text-input">
                            <label>Value / Delay (ms):</label>
                            <input type="text" id="step_value_input" placeholder="Nhập {{value}} hoặc 500">
                        </div>
                         <button id="add_step_btn" style="margin-top: 15px;">Thêm bước</button>
                    </div>
                    <div style="text-align: right; margin-top: 20px;">
                        <button id="save_workflow_btn">Lưu Workflow</button>
                        <button id="cancel_workflow_btn" style="margin-left: 10px;">Hủy</button>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(modal);

        // Event listeners for the modal
        document.getElementById('close-modal').onclick = closeWorkflowEditor;
        document.getElementById('cancel_workflow_btn').onclick = closeWorkflowEditor;
        document.getElementById('add_step_btn').onclick = addStepToEditor;
        document.getElementById('save_workflow_btn').onclick = saveWorkflowFromEditor;

        // Toggle panel button for the editor modal
        const editorPanelContent = document.getElementById('workflow-editor-content');
        const toggleEditorBtn = document.getElementById('toggle_editor_panel_btn');
        toggleEditorBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            const isCollapsed = editorPanelContent.classList.toggle('is-collapsed');
            toggleEditorBtn.innerHTML = isCollapsed ? '▼' : '▲';
            toggleEditorBtn.title = isCollapsed ? 'Mở rộng Panel' : 'Thu gọn Panel';
        });

        // Make the modal draggable
        makeDraggable(document.getElementById('workflow-editor-content'), document.getElementById('editor-header'));

        // Close modal when clicking outside of the content area
        window.onclick = function (event) {
            if (event.target == modal) {
                closeWorkflowEditor();
            }
        }
    }

    function registerHotkeys() {
        document.addEventListener('keydown', (e) => {
            // Do not trigger hotkeys if focus is in an input field or a text area,
            // or if the event originated from within our panels/modals
            if (e.target.closest('#workflow-engine-panel, #workflow-editor-modal') || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
                return;
            }
            if (e.key === 'ArrowRight' && !document.getElementById('next_btn').disabled) {
                e.preventDefault();
                document.getElementById('next_btn').click();
            }
            if (e.key === 'ArrowLeft' && !document.getElementById('prev_btn').disabled) {
                e.preventDefault();
                document.getElementById('prev_btn').click();
            }
            if (e.key === 'ArrowUp' && !document.getElementById('reselect_btn').disabled) {
                e.preventDefault();
                document.getElementById('reselect_btn').click();
            }
            if (e.key === 'ArrowDown' && !document.getElementById('run_workflow_btn').disabled) {
                e.preventDefault();
                document.getElementById('run_workflow_btn').click();
            }
        });
    }
    function navigate(direction, targetIndex = null) {
        if (isLooping) { log("Không thể điều hướng thủ công khi vòng lặp đang chạy.", "warn"); return; }
        if (permalinks.length === 0) return;
        let newIndex = (targetIndex !== null) ? targetIndex : (currentIndex + direction);
        if (newIndex >= 0 && newIndex < permalinks.length) {
            currentIndex = newIndex;
            updateUIState();
            processCurrentLink();
        } else {
            log('Đã đạt đến đầu/cuối danh sách.', 'warn');
        }
    }
    function processCurrentLink() {
        if (currentIndex < 0 || currentIndex >= permalinks.length) return log('Vị trí không hợp lệ.', 'warn');
        const url = permalinks[currentIndex];
        parseWazeUrlAndNavigate(url);
    }
    async function parseWazeUrlAndNavigate(url) {
        try {
            const parsedUrl = new URL(url);
            const params = parsedUrl.searchParams;
            const lon = parseFloat(params.get('lon'));
            const lat = parseFloat(params.get('lat'));
            // WME editor zoom levels are usually 1-2 levels higher than what's in the URL for direct map.setCenter
            const zoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10) + 2;

            const segmentIDs = (params.get('segments') || '').split(',').filter(id => id);
            const venueIDs = (params.get('venues') || '').split(',').filter(id => id);

            if (isNaN(lon) || isNaN(lat)) throw new Error('URL không chứa tọa độ (lon/lat) hợp lệ.');

            // Set map center and zoom first
            W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom);

            // Wait for model to be ready after map change, then select objects
            WazeWrap.Model.onModelReady(() => {
                (async () => {
                    await delay(1000); // Give WME some time to load objects after navigating
                    let objectsToSelect = [];

                    if (segmentIDs.length > 0) {
                        const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean);
                        if (segments.length === 0) {
                            log(`Cảnh báo: Không tìm thấy segment nào trên bản đồ từ ID ${segmentIDs.join(',')} sau khi tải.`, 'warn');
                        } else {
                            objectsToSelect.push(...segments);
                        }
                    }
                    if (venueIDs.length > 0) {
                        const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean);
                        if (venues.length === 0) {
                            log(`Cảnh báo: Không tìm thấy venue nào trên bản đồ từ ID ${venueIDs.join(',')} sau khi tải.`, 'warn');
                        } else {
                            objectsToSelect.push(...venues);
                        }
                    }

                    if (objectsToSelect.length > 0) {
                        W.selectionManager.setSelectedModels(objectsToSelect);
                    } else {
                        log('Cảnh báo: Không tìm thấy đối tượng nào trên bản đồ từ ID trong URL.', 'warn');
                    }
                })();
            }, true); // The 'true' ensures it runs once immediately if already ready, or on next ready event.
        } catch (error) {
            log(`Lỗi khi xử lý URL: ${error.message}`, 'error');
            console.error(error);
        }
    }

    function updateUIState() {
        const hasLinks = permalinks.length > 0;
        const navIndexInput = document.getElementById('nav_index_input');
        const navTotalCount = document.getElementById('nav_total_count');
        const workflowSelect = document.getElementById('workflow_select');
        const loopBtn = document.getElementById('loop_workflow_btn');

        document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0;
        document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1;
        document.getElementById('reselect_btn').disabled = !hasLinks;
        document.getElementById('run_workflow_btn').disabled = !hasLinks || !workflowSelect.value; // Disable run if no workflow selected

        navIndexInput.disabled = !hasLinks || isLooping;
        if (hasLinks) {
            navIndexInput.value = currentIndex + 1;
            navIndexInput.max = permalinks.length;
            navTotalCount.textContent = ` / ${permalinks.length}`;
            //document.getElementById('status_info').textContent = `Đang ở: ${currentIndex + 1}/${permalinks.length}`;
        } else {
            navIndexInput.value = '';
            navTotalCount.textContent = '/ N/A';
            //document.getElementById('status_info').textContent = 'Chưa có file nào được tải.';
        }

        document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0 || isLooping;
        document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1 || isLooping;
        document.getElementById('reselect_btn').disabled = !hasLinks || isLooping;
        document.getElementById('run_workflow_btn').disabled = !hasLinks || isLooping;
        document.getElementById('excel_file').disabled = isLooping;
        document.getElementById('workflow_select').disabled = isLooping;
        document.getElementById('workflow_variable_input').disabled = isLooping;
        document.getElementById('edit_workflow_btn').disabled = !workflowSelect.value || isLooping;
        document.getElementById('delete_workflow_btn').disabled = !workflowSelect.value || isLooping;
        document.getElementById('new_workflow_btn').disabled = isLooping;

        loopBtn.disabled = !hasLinks;
        if (isLooping) {
            loopBtn.textContent = '⏹️ Dừng Lặp';
            loopBtn.classList.add('looping');
            loopBtn.classList.remove('secondary');
        } else {
            loopBtn.textContent = '🔁 Bắt đầu Lặp';
            loopBtn.classList.remove('looping');
            loopBtn.classList.add('secondary');
        }
    }

    function populateWorkflowSelector() {
        const select = document.getElementById('workflow_select');
        const currentId = select.value; // Store current selection
        select.innerHTML = ''; // Clear existing options

        // Add a default "Select Workflow" option if no workflows are available or selected
        if (Object.keys(allWorkflows).length === 0) {
            const defaultOption = document.createElement('option');
            defaultOption.value = '';
            defaultOption.textContent = '--- Không có workflow ---';
            select.appendChild(defaultOption);
        } else {
            const emptyOption = document.createElement('option');
            emptyOption.value = '';
            emptyOption.textContent = '--- Chọn workflow ---';
            select.appendChild(emptyOption);
        }


        for (const id in allWorkflows) {
            const option = document.createElement('option');
            option.value = id;
            option.textContent = allWorkflows[id].name;
            select.appendChild(option);
        }

        // Restore previous selection or select the first valid one
        if (currentId && allWorkflows[currentId]) {
            select.value = currentId;
        } else if (Object.keys(allWorkflows).length > 0) {
            select.value = Object.keys(allWorkflows)[0]; // Select the first workflow if none were selected
        } else {
            select.value = ''; // No workflows, ensure no value is set
        }

        updateUIState(); // Update other UI elements based on selection
    }

    function deleteSelectedWorkflow() {
        const select = document.getElementById('workflow_select');
        const idToDelete = select.value;
        const workflowName = allWorkflows[idToDelete]?.name;

        if (!idToDelete) {
            alert("Vui lòng chọn một workflow để xóa.");
            return;
        }

        if (confirm(`Bạn có chắc chắn muốn xóa workflow "${workflowName}" không?`)) {
            delete allWorkflows[idToDelete];
            saveWorkflows();
            populateWorkflowSelector();
            log(`Đã xóa workflow: "${workflowName}"`, 'info');
        }
    }

    let draggedItem = null; // For drag-and-drop reordering of steps

    /**
     * Resets the step editor form to its default state.
     */
    function resetStepEditorForm() {
        document.querySelectorAll('#workflow_steps_list li.editing').forEach(el => el.classList.remove('editing'));
        document.getElementById('step_type_select').value = 'click';
        document.getElementById('step_desc_input').value = '';
        document.getElementById('step_selector_input').value = '';
        document.getElementById('step_shadow_input').value = '';
        document.getElementById('step_value_input').value = '';
        const addBtn = document.getElementById('add_step_btn');
        addBtn.textContent = 'Thêm bước';
        delete addBtn.dataset.editingIndex; // Remove the editing index
    }

    function openWorkflowEditor(workflowId = null) {
        const modal = document.getElementById('workflow-editor-modal');
        const title = document.getElementById('editor-title');
        const nameInput = document.getElementById('workflow_name_input');
        const idInput = document.getElementById('editing_workflow_id');
        const stepsList = document.getElementById('workflow_steps_list');

        stepsList.innerHTML = ''; // Clear previous steps
        resetStepEditorForm(); // Reset the add/edit step form

        if (workflowId && allWorkflows[workflowId]) {
            const wf = allWorkflows[workflowId];
            title.textContent = "Chỉnh sửa Workflow";
            nameInput.value = wf.name;
            idInput.value = workflowId;
            wf.steps.forEach((step, index) => renderStepInEditor(step, index));
        } else {
            title.textContent = "Tạo Workflow Mới";
            nameInput.value = '';
            idInput.value = '';
        }

        modal.style.display = 'block'; // Show the modal (block to allow centering)
    }

    function closeWorkflowEditor() {
        document.getElementById('workflow-editor-modal').style.display = 'none';
    }

    function renderStepInEditor(step, index) {
        const list = document.getElementById('workflow_steps_list');
        const li = document.createElement('li');
        li.dataset.index = index;
        li.dataset.step = JSON.stringify(step); // Store step data on the element
        li.draggable = true;

        li.innerHTML = `
        <div>
            <span class="step-number">${index + 1}.</span>
            <span><strong>${step.type.toUpperCase()}:</strong> ${step.desc || 'Không có mô tả'}</span>
        </div>
        <button class="delete-step-btn" title="Xóa bước này" style="background: none; border: none; color: #e74c3c; font-weight: bold; cursor: pointer; padding: 0;">✖</button>
    `;
        li.addEventListener('click', (e) => {
            // If delete button was clicked, don't enter edit mode
            if (e.target.classList.contains('delete-step-btn')) return;

            resetStepEditorForm(); // Reset any other editing state
            li.classList.add('editing'); // Mark this item as currently being edited

            const stepData = JSON.parse(li.dataset.step);
            document.getElementById('step_type_select').value = stepData.type;
            document.getElementById('step_desc_input').value = stepData.desc || '';
            document.getElementById('step_selector_input').value = stepData.selector || '';
            document.getElementById('step_shadow_input').value = stepData.shadowSelector || '';
            document.getElementById('step_value_input').value = stepData.type === 'delay' ? stepData.delay : (stepData.value || '');

            const addBtn = document.getElementById('add_step_btn');
            addBtn.textContent = `Cập nhật bước ${index + 1}`;
            addBtn.dataset.editingIndex = index; // Store the index of the step being edited
        });

        li.querySelector('.delete-step-btn').onclick = () => {
            li.remove();
            updateStepNumbers(); // Re-index steps after deletion
            resetStepEditorForm(); // Clear editor form if the deleted step was being edited
        };

        // Drag and drop event listeners
        li.addEventListener('dragstart', (e) => {
            draggedItem = e.target;
            e.target.classList.add('dragging'); // Add class for visual feedback
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/plain', index); // Set data for Firefox
            setTimeout(() => e.target.style.opacity = '0.5', 0); // Hide the original element slightly
        });

        li.addEventListener('dragend', (e) => {
            e.target.classList.remove('dragging');
            e.target.style.opacity = '1';
            draggedItem = null;
            updateStepNumbers(); // Update numbers and data-index after drop
        });

        li.addEventListener('dragover', (e) => {
            e.preventDefault(); // Allow drop
            const afterElement = getDragAfterElement(list, e.clientY);
            if (draggedItem && draggedItem !== e.target) { // Ensure it's not dragging over itself
                if (afterElement == null) {
                    list.appendChild(draggedItem);
                } else {
                    list.insertBefore(draggedItem, afterElement);
                }
            }
        });
        list.appendChild(li);
    }

    function updateStepNumbers() {
        const list = document.getElementById('workflow_steps_list');
        const currentEditingIndex = document.getElementById('add_step_btn').dataset.editingIndex;
        let newEditingIndex = -1;

        Array.from(list.children).forEach((li, i) => {
            li.dataset.index = i;
            li.querySelector('.step-number').textContent = `${i + 1}.`;
            const stepData = JSON.parse(li.dataset.step);
            // Re-render text content in case the type or description was changed on a dragged item
            li.querySelector('span:not(.step-number)').innerHTML = `<strong>${stepData.type.toUpperCase()}:</strong> ${stepData.desc || 'Không có mô tả'}`;

            if (li.classList.contains('editing')) {
                newEditingIndex = i;
            }
        });

        // Update the 'Add/Update Step' button's text if the edited item moved
        if (newEditingIndex !== -1) {
            const addBtn = document.getElementById('add_step_btn');
            addBtn.textContent = `Cập nhật bước ${newEditingIndex + 1}`;
            addBtn.dataset.editingIndex = newEditingIndex; // Update the stored index
        }
    }

    function getDragAfterElement(container, y) {
        const draggableElements = [...container.querySelectorAll('li:not(.dragging)')];
        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = y - box.top - box.height / 2;
            if (offset < 0 && offset > closest.offset) {
                return { offset: offset, element: child };
            } else {
                return closest;
            }
        }, { offset: Number.NEGATIVE_INFINITY }).element;
    }

    function addStepToEditor() {
        const addBtn = document.getElementById('add_step_btn');
        const editingIndex = addBtn.dataset.editingIndex; // This will be undefined if adding a new step

        const stepData = {
            type: document.getElementById('step_type_select').value,
            desc: document.getElementById('step_desc_input').value.trim()
        };
        const selector = document.getElementById('step_selector_input').value.trim();
        const shadowSelector = document.getElementById('step_shadow_input').value.trim();
        const valueOrDelay = document.getElementById('step_value_input').value.trim();

        if (selector) stepData.selector = selector;
        if (shadowSelector) stepData.shadowSelector = shadowSelector;

        if (stepData.type === 'delay') {
            stepData.delay = parseInt(valueOrDelay, 10) || 100; // Default to 100ms
        } else if (stepData.type === 'input') {
            if (valueOrDelay) stepData.value = valueOrDelay;
            else { alert("Vui lòng nhập giá trị cho bước Input."); return; }
        } else if (stepData.type === 'log') {
            // Log step doesn't require selector or value beyond description
            stepData.value = valueOrDelay; // Can use value for log message if desired
            delete stepData.selector;
            delete stepData.shadowSelector;
        }

        // Basic validation
        if (!stepData.desc) { alert("Vui lòng nhập mô tả ngắn cho bước này."); return; }
        if ((stepData.type === 'click' || stepData.type === 'input') && !stepData.selector) { alert("Vui lòng nhập Selector (JS Path)."); return; }


        if (editingIndex !== undefined && editingIndex !== '') { // If editing an existing step
            const list = document.getElementById('workflow_steps_list');
            const liToUpdate = list.querySelector(`li[data-index='${editingIndex}']`);
            if (liToUpdate) {
                liToUpdate.dataset.step = JSON.stringify(stepData); // Update stored data
                liToUpdate.querySelector('span:not(.step-number)').innerHTML = `<strong>${stepData.type.toUpperCase()}:</strong> ${stepData.desc || 'Không có mô tả'}`;
            }
        } else { // Adding a new step
            const list = document.getElementById('workflow_steps_list');
            renderStepInEditor(stepData, list.children.length); // Add to the end
        }
        resetStepEditorForm();
    }

    function saveWorkflowFromEditor() {
        const name = document.getElementById('workflow_name_input').value.trim();
        if (!name) {
            alert("Vui lòng nhập tên cho workflow.");
            return;
        }

        const steps = [];
        document.querySelectorAll('#workflow_steps_list li').forEach(li => {
            steps.push(JSON.parse(li.dataset.step));
        });

        if (steps.length === 0) {
            alert("Workflow phải có ít nhất một bước.");
            return;
        }

        let id = document.getElementById('editing_workflow_id').value;
        if (!id) {
            // Generate a unique ID for new workflows
            id = `custom_${Date.now()}`;
        }
        allWorkflows[id] = { name, steps };
        saveWorkflows();
        populateWorkflowSelector();
        document.getElementById('workflow_select').value = id; // Select the newly saved workflow
        closeWorkflowEditor();
        log(`Đã lưu workflow "${name}"`, 'success');
    }

    // Initialize the script
    bootstrap();
})();