GPE Helper

Adds quality-of-life features for Google Product Experts.

// ==UserScript==
// @name         GPE Helper
// @namespace    https://github.com/gncnpk/GPE-Helper
// @version      0.0.5
// @description  Adds quality-of-life features for Google Product Experts.
// @author       Gavin Canon-Phratsachack (https://github.com/gncnpk)
// @match        https://support.google.com/*/thread*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=productexperts.withgoogle.com
// @grant        GM_xmlhttpRequest
// @license      MIT
// @connect      raw.githubusercontent.com
// @connect      github.com
// ==/UserScript==

(function() {
    'use strict';

    let templateResponses = {};
    let currentProduct = null;

    // GitHub responses URL
    const RESPONSES_URL = 'https://raw.githubusercontent.com/gncnpk/gpe-helper/refs/heads/main/responses.json';

    // Position management
    const POSITION_KEY = 'gpe-helper-position';
    const COLLAPSED_KEY = 'gpe-helper-collapsed';

    function detectProduct() {
        const url = window.location.href;
        const match = url.match(/support\.google\.com\/([^\/]+)\/thread/);
        return match ? match[1] : null;
    }

    function fetchResponses() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: RESPONSES_URL,
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            resolve(data);
                        } catch (error) {
                            console.error('Failed to parse JSON:', error);
                            reject(error);
                        }
                    } else {
                        reject(new Error(`HTTP error! status: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    console.error('Request failed:', error);
                    reject(error);
                }
            });
        });
    }

    function savePosition(x, y) {
        localStorage.setItem(POSITION_KEY, JSON.stringify({
            x,
            y
        }));
    }

    function loadPosition() {
        const saved = localStorage.getItem(POSITION_KEY);
        if (saved) {
            return JSON.parse(saved);
        }
        return {
            x: 20,
            y: 20
        }; // Default position (top-right)
    }

    function saveCollapsedState(isCollapsed) {
        localStorage.setItem(COLLAPSED_KEY, isCollapsed.toString());
    }

    function loadCollapsedState() {
        const saved = localStorage.getItem(COLLAPSED_KEY);
        return saved === 'true';
    }

    function makeDraggable(panel, header, initialPosition) {
        let isDragging = false;
        let currentX = initialPosition.x;
        let currentY = initialPosition.y;
        let initialX = 0;
        let initialY = 0;
        let xOffset = initialPosition.x;
        let yOffset = initialPosition.y;

        function dragStart(e) {
            // Only allow dragging from header, not from the toggle arrow
            if (e.target.id === 'gpe-toggle-arrow') {
                return;
            }

            if (e.type === "touchstart") {
                initialX = e.touches[0].clientX - xOffset;
                initialY = e.touches[0].clientY - yOffset;
            } else {
                initialX = e.clientX - xOffset;
                initialY = e.clientY - yOffset;
            }

            if (e.target === header || header.contains(e.target)) {
                isDragging = true;
                header.style.cursor = 'grabbing';
            }
        }

        function dragEnd(e) {
            if (isDragging) {
                initialX = currentX;
                initialY = currentY;
                isDragging = false;
                header.style.cursor = 'grab';

                // Save position
                savePosition(currentX, currentY);
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();

                if (e.type === "touchmove") {
                    currentX = e.touches[0].clientX - initialX;
                    currentY = e.touches[0].clientY - initialY;
                } else {
                    currentX = e.clientX - initialX;
                    currentY = e.clientY - initialY;
                }

                xOffset = currentX;
                yOffset = currentY;

                // Keep panel within viewport bounds
                const rect = panel.getBoundingClientRect();
                const viewportWidth = window.innerWidth;
                const viewportHeight = window.innerHeight;

                currentX = Math.max(0, Math.min(currentX, viewportWidth - rect.width));
                currentY = Math.max(0, Math.min(currentY, viewportHeight - rect.height));

                panel.style.transform = `translate(${currentX}px, ${currentY}px)`;
            }
        }

        // Add event listeners
        header.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        // Touch events for mobile
        header.addEventListener('touchstart', dragStart);
        document.addEventListener('touchmove', drag);
        document.addEventListener('touchend', dragEnd);

        // Set initial cursor
        header.style.cursor = 'grab';
    }

    function waitForElm(selector, doc) {
        return new Promise(resolve => {
            if (doc.querySelector(selector)) {
                return resolve(doc.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (doc.querySelector(selector)) {
                    observer.disconnect();
                    resolve(doc.querySelector(selector));
                }
            });

            try {
                observer.observe(doc.body, {
                    childList: true,
                    subtree: true
                });
            } catch {
                observer.observe(doc, {
                    childList: true,
                    subtree: true
                });
            }
        });
    }

    function getTimeOfDay() {
        const hour = new Date().getHours();
        if (hour < 12) return 'morning';
        if (hour < 18) return 'afternoon';
        return 'evening';
    }

    function createTemplatePanel() {
        const position = loadPosition();
        const isCollapsed = loadCollapsedState();

        const panel = document.createElement('div');
        panel.id = 'gpe-template-panel';
        panel.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 300px;
            transform: translate(${position.x}px, ${position.y}px);
            user-select: none;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            padding: 12px 16px;
            background: #f8f9fa;
            border-bottom: 1px solid #ddd;
            border-radius: 8px 8px 0 0;
            font-weight: 600;
            font-size: 14px;
            color: #333;
            cursor: grab;
            display: flex;
            justify-content: space-between;
            align-items: center;
            user-select: none;
        `;

        const content = document.createElement('div');
        content.id = 'gpe-template-content';
        content.style.cssText = `
            padding: 8px;
            display: ${isCollapsed ? 'none' : 'block'};
        `;

        // Show loading state initially
        header.innerHTML = `
            <span>GPE Helper - Loading...</span>
            <span id="gpe-toggle-arrow" style="font-size: 12px; transition: transform 0.2s; cursor: pointer;">▼</span>
        `;

        // Check if we have responses to show
        if (Object.keys(templateResponses).length === 0) {
            content.innerHTML = `
                <div style="padding: 8px; text-align: center; color: #666; font-size: 12px;">
                    ${currentProduct ? `No templates available for "${currentProduct}"` : 'Unable to detect product or load templates'}
                </div>
            `;
        } else {
            // Create buttons for each template
            Object.keys(templateResponses).forEach(templateName => {
                const button = document.createElement('button');
                button.textContent = templateName;
                button.style.cssText = `
                    display: block;
                    width: 100%;
                    padding: 8px 12px;
                    margin: 4px 0;
                    background: #4285f4;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 12px;
                    transition: background-color 0.2s;
                `;

                button.addEventListener('mouseenter', () => {
                    button.style.backgroundColor = '#3367d6';
                });

                button.addEventListener('mouseleave', () => {
                    button.style.backgroundColor = '#4285f4';
                });

                button.addEventListener('click', () => {
                    appendTemplateToEditor(templateResponses[templateName]);
                });

                content.appendChild(button);
            });
        }

        // Update header text
        const productName = currentProduct ? currentProduct.charAt(0).toUpperCase() + currentProduct.slice(1) : 'Unknown';
        header.innerHTML = `
            <span>GPE Helper - ${productName}</span>
            <span id="gpe-toggle-arrow" style="font-size: 12px; transition: transform 0.2s; cursor: pointer;">▼</span>
        `;

        // Add toggle functionality
        let collapsed = isCollapsed;
        const toggleArrow = header.querySelector('#gpe-toggle-arrow');

        // Set initial arrow direction
        toggleArrow.style.transform = collapsed ? 'rotate(-90deg)' : 'rotate(0deg)';

        toggleArrow.addEventListener('click', (e) => {
            e.stopPropagation(); // Prevent drag from starting
            collapsed = !collapsed;
            content.style.display = collapsed ? 'none' : 'block';
            toggleArrow.style.transform = collapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
            saveCollapsedState(collapsed);
        });

        panel.appendChild(header);
        panel.appendChild(content);
        document.body.appendChild(panel);

        // Make the panel draggable - pass the initial position
        makeDraggable(panel, header, position);
    }

    function textToHtml(text) {
        // Convert plain text with \n to HTML with <br> tags
        // Handle escaped newlines from JSON
        const unescapedText = text.replace(/\\n/g, '\n');
        return unescapedText.split('\n').map(line => {
            if (line.trim() === '') {
                return '<br>';
            }
            return line;
        }).join('<br>');
    }

    function insertHtmlAtCursor(element, htmlText) {
        const selection = window.getSelection();

        // If no selection or selection is not in our editor, append at end
        if (!selection.rangeCount || !element.contains(selection.anchorNode)) {
            element.focus();
            const range = document.createRange();
            range.selectNodeContents(element);
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }

        const range = selection.getRangeAt(0);

        // Delete any selected content first
        range.deleteContents();

        // Create a temporary div to parse HTML
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlText;

        // Collect all nodes first to preserve order
        const nodes = Array.from(tempDiv.childNodes);

        // Insert nodes in correct order
        nodes.forEach(node => {
            range.insertNode(node);
            range.setStartAfter(node);
        });

        // Move cursor to end of inserted content
        range.collapse(false);
        selection.removeAllRanges();
        selection.addRange(range);
    }

    function needsSpacingBefore(editor) {
        const selection = window.getSelection();
        if (!selection.rangeCount) return false;

        const range = selection.getRangeAt(0);
        const textBefore = range.startContainer.textContent?.substring(0, range.startOffset) || '';

        // Check if we need spacing (content exists and doesn't end with whitespace)
        return textBefore.trim() && !textBefore.match(/\s{2,}$/);
    }

    function appendTemplateToEditor(templateText) {
        const editor = document.getElementsByClassName(
            'scTailwindSharedRichtexteditoreditor'
        )[0];

        if (editor) {
            // Focus the editor first
            editor.focus();

            // Convert template text to HTML
            let htmlToInsert = textToHtml(templateText);

            // Add spacing before template if needed
            if (needsSpacingBefore(editor)) {
                htmlToInsert = '<br><br>' + htmlToInsert;
            }

            insertHtmlAtCursor(editor, htmlToInsert);

            // Trigger input event to ensure the content is recognized
            const event = new Event('input', {
                bubbles: true
            });
            editor.dispatchEvent(event);
        }
    }

    async function init() {
        // Detect the current product
        currentProduct = detectProduct();
        console.log('Detected product:', currentProduct);

        // Fetch responses from GitHub
        try {
            const allResponses = await fetchResponses();

            if (allResponses && currentProduct && allResponses[currentProduct]) {
                templateResponses = allResponses[currentProduct];
                console.log(`Loaded responses for ${currentProduct}`, templateResponses);
            } else {
                console.warn('No responses found for product:', currentProduct);
            }
        } catch (error) {
            console.error('Failed to load responses:', error);
        }
        if (document.location.pathname.endsWith("threads")) {
            await waitForElm('.thread-list-counts__count--reply', document);
            document.querySelectorAll(".thread-list-counts__count--reply").forEach((a) => {
                if (a.innerText === "0 Replies") {
                    a.parentElement.parentElement.parentElement.parentElement.style = "background-color: rgba(255,0,0,0.1);"
                }
            })
        }
        await waitForElm('.scTailwindSharedRichtexteditoreditor', document);
        prefillResponse();
        createTemplatePanel();
    }

    function prefillResponse() {
        const timeOfDay = getTimeOfDay();
        const userElem = document.querySelector(
            '.scTailwindThreadPost_headerUserinfoname'
        );
        const username = userElem ? userElem.innerText.trim() : '';

        const editor = document.getElementsByClassName(
            'scTailwindSharedRichtexteditoreditor'
        )[0];

        editor.innerText = `Good ${timeOfDay} ${username},\n\nWaze staff does not monitor this forum.\n\n[Response here]\n\nIf there's anything else I can assist you with, please let me know!\n\nI'm not affiliated with either Waze or Google; I'm a volunteer product expert providing answers about Google Maps and Waze.`;
    }

    init();
})();