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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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

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

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

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

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

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

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         WME Workflow Engine
// @namespace    https://greasyfork.org/
// @version      2.0.7
// @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; // Biến này chỉ ra rằng vòng lặp đang hoạt động hoặc được yêu cầu dừng
    const DELAY_BETWEEN_LOOPS = 300;
    const STORAGE_KEY = 'wme_custom_workflows';

    const defaultWorkflows = {
        "street_city_update": {
            name: "Cập nhật Thành phố (Street)",
            steps: [
                { type: 'click', selector: '.w-icon.w-icon-pencil-fill.edit-button', desc: "Click nút Edit Address" },
                { type: 'delay', delay: 500, 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(1) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete', shadowSelector: 'div > wz-menu > wz-menu-item:nth-child(2) > div', 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 Thành 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); // Small delay for shadow DOM content to fully render
                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');
            throw error; // Re-throw to propagate the error
        }
    }

    /**
     * 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 })); // Some elements react to 'change'
                        } 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':
                    {
                        log(step.value.replace('{{value}}', variableValue), 'info');
                        break;
                    }
                default:
                    log(`Loại step không xác định: ${step.type}`, 'error');
                    throw new Error(`Loại step không xác định: ${step.type}`);
            }
        } catch (err) {
            log(`❌ Lỗi khi thực thi step (${step.type} - "${step.desc}"): ${err.message}`, 'error');
            throw err;
        }
    }

    /**
     * Chạy toàn bộ workflow đã chọn.
     * @param {boolean} isCalledByLoop - True nếu được gọi từ vòng lặp, false nếu là chạy đơn lẻ.
     */
    async function runSelectedWorkflow(isCalledByLoop = false) {
        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ệ!");
            if (isCalledByLoop) { // If called by loop, throw to stop the loop
                throw new Error("Không có workflow hợp lệ được chọn.");
            }
            return;
        }

        const workflow = allWorkflows[workflowId];
        log(`--- Bắt đầu chạy Workflow: "${workflow.name}" ---`, 'special');

        try {
            for (const step of workflow.steps) {
                // Kiểm tra yêu cầu dừng nếu đang trong vòng lặp tự động
                if (isCalledByLoop && !isLooping) {
                    log(`Workflow "${workflow.name}" đã bị dừng bởi người dùng.`, 'warn');
                    throw new Error("Workflow stopped by user."); // Propagate stop signal
                }
                await executeStep(step, variableValue);
            }
            log(`--- ✅ Hoàn thành Workflow: "${workflow.name}" ---`, 'success');
        } catch (error) {
            if (isCalledByLoop) { // Nếu là một phần của vòng lặp, hãy truyền lỗi/yêu cầu dừng
                if (isLooping) { // Lỗi thực sự trong vòng lặp
                    log(`--- ❌ Workflow "${workflow.name}" đã dừng do lỗi: ${error.message} ---`, 'error');
                } else { // Yêu cầu dừng do người dùng trong vòng lặp
                    log(`--- Workflow "${workflow.name}" đã dừng. ---`, 'warn');
                }
                throw error; // Ném lại lỗi để `executeLoop` xử lý
            } else { // Chạy đơn lẻ, chỉ ghi log lỗi và kết thúc
                log(`--- ❌ Workflow "${workflow.name}" đã dừng: ${error.message} ---`, 'error');
            }
        }
    }

    async function toggleWorkflowLoop() {
        if (isLooping) {
            // Đang chạy, yêu cầu dừng
            isLooping = false;
            log("Đã yêu cầu dừng vòng lặp. Sẽ dừng sau khi hoàn thành hoặc giữa bước hiện tại.", 'warn');
            // updateUIState() sẽ được gọi bởi executeLoop khi nó thực sự dừng
        } else {
            // Không chạy, bắt đầu vòng lặp
            if (permalinks.length === 0) {
                log("Vui lòng tải một file Excel/CSV trước khi bắt đầu vòng lặp.", 'warn');
                return;
            }
            isLooping = true; // Thiết lập trạng thái đang lặp
            updateUIState(); // Cập nhật UI để hiển thị nút "Dừng Lặp"
            log("--- Bắt đầu vòng lặp tự động ---", 'special');
            await executeLoop();
        }
    }

    async function executeLoop() {
        // Đảm bảo currentIndex hợp lệ trước khi bắt đầu vòng lặp
        if (currentIndex < 0 && permalinks.length > 0) {
            currentIndex = 0;
        }

        while (isLooping && currentIndex < permalinks.length) {
            updateUIState(); // Cập nhật UI để hiển thị đúng index và trạng thái

            try {
                // Điều hướng và chọn đối tượng
                log(`Đang xử lý mục ${currentIndex + 1}/${permalinks.length}: ${permalinks[currentIndex]}`, 'info');
                await processCurrentLink();

                if (!isLooping) { break; } // Kiểm tra yêu cầu dừng sau điều hướng
                await delay(1500); // Chờ cho WME ổn định sau khi điều hướng

                if (!isLooping) { break; } // Kiểm tra yêu cầu dừng trước khi chạy workflow
                await runSelectedWorkflow(true); // Truyền `true` để chỉ ra rằng workflow được gọi từ vòng lặp

            } catch (error) {
                // Nếu `runSelectedWorkflow` ném ra lỗi do người dùng dừng hoặc lỗi thực thi
                if (isLooping) { // Nếu `isLooping` vẫn `true`, đây là một lỗi thực sự trong quá trình xử lý
                    log(`Lỗi ở mục ${currentIndex + 1}, bỏ qua và tiếp tục. Lỗi: ${error.message}`, 'error');
                } else { // Đây là một yêu cầu dừng của người dùng, không cần ghi log như lỗi
                    log(`Vòng lặp đã dừng tại mục ${currentIndex + 1} do yêu cầu dừng của người dùng.`, 'warn');
                }
                break; // Thoát vòng lặp khi có lỗi hoặc yêu cầu dừng
            }

            // Kiểm tra lần nữa nếu người dùng đã nhấn dừng trong lúc workflow chạy hoặc delay
            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');
                if (!isLooping) { break; } // Kiểm tra trước khi delay
                await delay(DELAY_BETWEEN_LOOPS);
            } else {
                log("Đã đến mục cuối cùng của danh sách.", 'info');
                isLooping = false; // Đảm bảo vòng lặp kết thúc khi đến cuối
            }
        }

        // Cleanup cuối cùng sau khi vòng lặp kết thúc (tự nhiên hoặc do dừng)
        isLooping = false; // Đảm bảo `isLooping` là `false` khi vòng lặp hoàn toàn thoát
        if (currentIndex >= permalinks.length && permalinks.length > 0) {
            log("--- ✅ Hoàn thành vòng lặp tự động! ---", 'special');
        } else if (permalinks.length === 0) {
            log("Không có permalink nào để lặp.", 'warn');
        } else { // Trường hợp này xảy ra nếu vòng lặp bị `break` do người dùng dừng hoặc lỗi
            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');
            updateUIState();
            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;
                    log(`Đã tải thành công ${permalinks.length} permalink.`, 'success');
                    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, "&lt;").replace(/>/g, "&gt;")}`; // 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.95); border: 1px solid #ccc;
    padding: 0; z-index: 1001; border-radius: 8px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 13px; width: 420px;
    `;
        panel.innerHTML = `
    <h3 id="navigator-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 3px 8px; 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%; margin-top: 10px;" 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: 5px 0 5px 0;">
                        <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;
    }

    /* 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: 15px; padding: 5px 12px; }
    #workflow-engine-panel .action-btn { 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;}

    /* Buttons layout in workflow editor */
    .workflow-btns {
      display: flex;
      justify-content: flex-end; /* Push buttons to the right */
      gap: 10px; /* Space between buttons */
      margin-top: 15px;
    }
    .workflow-btns button:first-child {
        margin-right: auto; /* Push the first button to the left */
    }


    /* UI Căn chỉnh cho các form group */
    .wwe-form-group {
        display: flex;
        flex-direction: column;
        margin-bottom: 8px;
    }
    .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; background-color: white; /* Simplified padding */
        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;
        border: 1px solid #888;
        width: 80%; /* Increased width for better usability */
        max-width: 700px; /* Increased max-width */
        border-radius: 8px;
        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;
        padding: 3px 8px !important;
        cursor: grab;
        border-bottom: 1px solid #eee;
    }

    #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: 5px;
        border-radius: 4px;
    }
    #workflow_steps_list li {
        position: relative;
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 5px 8px; /* Adjusted padding */
        border: 1px solid #ddd;
        margin-bottom: 3px;
        border-radius: 4px;
        background: #fafafa;
        cursor: grab;
    }
    #workflow_steps_list li .step-number {
        margin-right: 8px;
        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;
    }
`;
        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 {
                    // Set height based on scrollHeight, plus a little buffer if needed
                    content.style.maxHeight = content.scrollHeight + 10 + "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(false)); // Chạy đơn lẻ
        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.innerHTML = `
            <div id="workflow-editor-content">
                <h3 id="editor-header">
                    <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: 10px;">
                    <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: 5px; 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 (CSS):</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>
                        <div class="workflow-btns">
                            <button id="add_step_btn">Thêm bước</button>
                            <button id="save_workflow_btn" class="primary">Lưu Workflow</button>
                            <button id="cancel_workflow_btn" class="secondary">Hủy</button>
                        </div>
                    </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) {
             log('Vị trí không hợp lệ hoặc chưa có permalink nào được tải.', 'warn');
             return;
        }
        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
            // Use WazeWrap.Model.onModelReady with a one-time execution flag
            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);
                        log(`Đã chọn ${objectsToSelect.length} đối tượng từ URL.`, 'info');
                    } 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. Vui lòng kiểm tra lại permalink.', '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');
        const runWorkflowBtn = document.getElementById('run_workflow_btn');

        // Navigation buttons (only enabled if permalinks exist and not looping)
        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;

        // Index input
        navIndexInput.disabled = !hasLinks || isLooping;
        if (hasLinks) {
            navIndexInput.value = currentIndex + 1;
            navIndexInput.max = permalinks.length;
            navTotalCount.textContent = ` / ${permalinks.length}`;
        } else {
            navIndexInput.value = '';
            navTotalCount.textContent = '/ N/A';
        }

        // Workflow action buttons
        runWorkflowBtn.disabled = !workflowSelect.value || isLooping;
        loopBtn.disabled = !hasLinks; // Nút lặp luôn có thể được nhấn nếu có permalink

        // Workflow editor related buttons
        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;

        // Loop button visual state
        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');
            if (hasLinks) loopBtn.classList.add('secondary'); // Chỉ thêm secondary nếu có permalink
        }
    }

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

        const emptyOption = document.createElement('option');
        emptyOption.value = '';
        emptyOption.textContent = Object.keys(allWorkflows).length === 0 ? '--- Không có workflow ---' : '--- 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 the first workflow if none were selected, but prefer the empty option if it exists
            const firstWorkflowId = Object.keys(allWorkflows)[0];
            if (firstWorkflowId) {
                select.value = firstWorkflowId;
            }
        } 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 (can be anything)
            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 addBtn = document.getElementById('add_step_btn');
        let currentEditingIndex = addBtn.dataset.editingIndex ? parseInt(addBtn.dataset.editingIndex, 10) : -1;
        let foundNewEditingIndex = -1;

        Array.from(list.children).forEach((li, i) => {
            li.dataset.index = i;
            li.querySelector('.step-number').textContent = `${i + 1}.`;
            // Keep existing step data, no need to re-parse or re-render unless values change
            // Just update display of description if it exists
            const stepData = JSON.parse(li.dataset.step);
            li.querySelector('span:not(.step-number)').innerHTML = `<strong>${stepData.type.toUpperCase()}:</strong> ${stepData.desc || 'Không có mô tả'}`;

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

        // Update the 'Add/Update Step' button's text if the edited item moved
        if (foundNewEditingIndex !== -1 && foundNewEditingIndex !== currentEditingIndex) {
            addBtn.textContent = `Cập nhật bước ${foundNewEditingIndex + 1}`;
            addBtn.dataset.editingIndex = foundNewEditingIndex; // Update the stored index
        } else if (foundNewEditingIndex === -1 && currentEditingIndex !== -1) {
            // If the edited item was deleted or un-edited
            resetStepEditorForm();
        }
    }

    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 ? parseInt(addBtn.dataset.editingIndex, 10) : undefined;

        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
            if (isNaN(stepData.delay)) { alert("Giá trị Delay phải là số."); return; }
        } 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') {
            stepData.value = valueOrDelay; // Value is the log message
            delete stepData.selector; // Log type doesn't use selector/shadowSelector
            delete stepData.shadowSelector;
        }

        // Basic validation for common step types
        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 (CSS)."); return; }


        if (editingIndex !== undefined) { // 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();
        updateStepNumbers(); // Ensure all numbers are correct after add/update
    }

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