Perplexity Model Watcher

Observer for the actual model used in Perplexity. Elegant interface, drag-and-drop, model mismatch detection.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Perplexity Model Watcher
// @name:ru      Perplexity модель ответа
// @namespace    http://tampermonkey.net/
// @version      1.0
// @license MIT
// @description  Observer for the actual model used in Perplexity. Elegant interface, drag-and-drop, model mismatch detection.
// @description:ru Наблюдатель за реальной моделью в Perplexity. Элегантный интерфейс, перетаскивание, детектор подмены модели.
// @author       MaxScorpy
// @match        https://*.perplexity.ai/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // Configuration & State
    const CONFIG = { apiDelay: 1000 };
    const STATE = { lastDisplay: null, lastSelected: null };
    const STORE_KEY_POS = 'mw_pos_release_v1'; // Storage key for position

    // --- 1. CSS: Styling (Dark Glassmorphism) ---
    function injectStyles() {
        if (document.getElementById('mw-perfect-style')) return;
        const css = `
            /* Animations */
            @keyframes mw-glow-red {
                0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); border-color: rgba(239, 68, 68, 0.2); }
                50% { box-shadow: 0 0 12px 2px rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.5); }
                100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); border-color: rgba(239, 68, 68, 0.2); }
            }
            @keyframes mw-glow-green {
                0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
                50% { box-shadow: 0 0 12px 1px rgba(16, 185, 129, 0.2); }
                100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
            }

            /* Main Container (Pill) */
            #mw-pill {
                position: fixed;
                z-index: 99999;
                /* Default position */
                bottom: 30px;
                left: 50%;
                /* Note: Transform is used for centering unless dragged. JS handles this. */

                display: flex;
                align-items: center;
                justify-content: center;

                height: 36px;
                padding: 0 20px;
                gap: 10px;

                /* Visual Style (Dark & Neon) */
                background: rgba(15, 15, 15, 0.85);
                backdrop-filter: blur(16px);
                -webkit-backdrop-filter: blur(16px);
                border: 1px solid rgba(255, 255, 255, 0.08);
                border-radius: 99px;

                font-family: 'Inter', -apple-system, sans-serif;
                font-size: 13px;
                color: #ececec;

                cursor: grab;
                user-select: none;
                box-shadow: 0 10px 25px rgba(0,0,0,0.5);
                transition: width 0.3s, background 0.3s, border-color 0.3s, opacity 0.3s;

                opacity: 0; /* Hidden until initialized */
            }

            #mw-pill.mw-visible { opacity: 1; }
            #mw-pill:active { cursor: grabbing; }

            /* Status Dot */
            .mw-dot {
                width: 6px; height: 6px;
                border-radius: 50%;
                background: #555;
                flex-shrink: 0;
                transition: 0.3s;
            }

            /* Content Text */
            .mw-content {
                display: flex;
                align-items: center;
                gap: 8px;
                white-space: nowrap;
                transform: translateX(0px);
            }
            .mw-model { font-weight: 500; letter-spacing: 0.3px; }
            .mw-arrow { color: #888; font-size: 11px; margin: 0 2px; }
            .mw-err { color: #fca5a5; text-shadow: 0 0 10px rgba(239, 68, 68, 0.4); }

            /* Close/Minimize Button (Absolute to prevent layout shift) */
            .mw-close-btn {
                position: absolute;
                right: 6px;
                width: 24px; height: 24px;
                border-radius: 50%;
                background: rgba(255,255,255,0.1);
                color: #fff;
                display: flex; align-items: center; justify-content: center;
                font-size: 14px;
                line-height: 1;
                cursor: pointer;

                opacity: 0;
                transform: scale(0.8);
                transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
                pointer-events: none;
            }

            .mw-close-btn:hover { background: rgba(255,255,255,0.25); transform: scale(1.1); }

            /* Show button on hover */
            #mw-pill:hover .mw-close-btn {
                opacity: 1;
                transform: scale(1);
                pointer-events: auto;
            }

            /* Status States */
            #mw-pill.st-ok { animation: mw-glow-green 4s infinite; border-color: rgba(16, 185, 129, 0.3); }
            #mw-pill.st-ok .mw-dot { background: #10b981; box-shadow: 0 0 8px #10b981; }

            #mw-pill.st-bad { animation: mw-glow-red 2s infinite; }
            #mw-pill.st-bad .mw-dot { background: #ef4444; box-shadow: 0 0 8px #ef4444; }

            /* Minimized State */
            #mw-pill.mw-minimized {
                width: 36px; /* Circle */
                padding: 0;
                justify-content: center;
                gap: 0;
            }
            #mw-pill.mw-minimized .mw-content { display: none; }
            #mw-pill.mw-minimized .mw-close-btn { display: none; }

        `;
        const s = document.createElement('style');
        s.id = 'mw-perfect-style';
        s.textContent = css;
        document.head.appendChild(s);
    }

    // --- 2. Logic: Create, Drag, Restore ---
    function createPill() {
        if (document.getElementById('mw-pill')) return;

        const pill = document.createElement('div');
        pill.id = 'mw-pill';
        // Insert structure
        pill.innerHTML = `
            <div class="mw-dot"></div>
            <div class="mw-content" id="mw-text">Waiting...</div>
            <div class="mw-close-btn" title="Minimize">×</div>
        `;

        document.body.appendChild(pill);
        restoreState(pill);

        // Fade in animation
        requestAnimationFrame(() => pill.classList.add('mw-visible'));

        // --- Drag Logic ---
        let isDragging = false;
        let startX, startY, initLeft, initTop;

        pill.addEventListener('mousedown', (e) => {
            if (e.target.classList.contains('mw-close-btn')) return;
            if (e.button !== 0) return; // Left click only

            isDragging = true;
            pill.style.transition = 'none'; // Disable transition during drag

            // If element was centered via transform, switch to absolute coordinates on first drag
            const rect = pill.getBoundingClientRect();

            pill.style.transform = 'none';
            pill.style.bottom = 'auto';
            pill.style.left = rect.left + 'px';
            pill.style.top = rect.top + 'px';

            startX = e.clientX;
            startY = e.clientY;
            initLeft = rect.left;
            initTop = rect.top;

            e.preventDefault();
        });

        window.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            pill.style.left = (initLeft + dx) + 'px';
            pill.style.top = (initTop + dy) + 'px';
        });

        window.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                pill.style.transition = ''; // Re-enable animations
                saveState(pill);
            }
        });

        // --- Minimize Logic ---
        const closeBtn = pill.querySelector('.mw-close-btn');
        closeBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            pill.classList.add('mw-minimized');
            saveState(pill);
        });

        // Expand on click
        pill.addEventListener('click', (e) => {
            if (pill.classList.contains('mw-minimized')) {
                pill.classList.remove('mw-minimized');
                saveState(pill);
            }
        });
    }

    function saveState(el) {
        const rect = el.getBoundingClientRect();
        const state = {
            top: rect.top,
            left: rect.left,
            minimized: el.classList.contains('mw-minimized')
        };
        GM_setValue(STORE_KEY_POS, state);
    }

    function restoreState(el) {
        const state = GM_getValue(STORE_KEY_POS);
        if (state) {
            // Restore position
            el.style.transform = 'none';
            el.style.bottom = 'auto';
            el.style.top = state.top + 'px';
            el.style.left = state.left + 'px';

            if (state.minimized) el.classList.add('mw-minimized');
        } else {
            // Default: Center bottom
            el.style.transform = 'translateX(-50%)';
        }
    }

    // --- 3. Data & Update ---
    function updateUI(disp, sel) {
        STATE.lastDisplay = disp;
        STATE.lastSelected = sel;

        const pill = document.getElementById('mw-pill');
        const textContainer = document.getElementById('mw-text');
        if (!pill || !textContainer) return;

        pill.classList.remove('st-ok', 'st-bad', 'st-wait');

        if (!disp && !sel) {
            pill.classList.add('st-wait');
            textContainer.innerHTML = `<span style="opacity:0.5">Analyzing...</span>`;
            return;
        }

        if (disp === sel) {
            // OK
            pill.classList.add('st-ok');
            textContainer.innerHTML = `<span class="mw-model">${disp}</span>`;
        } else {
            // MISMATCH: Selected -> Arrow -> Display
            pill.classList.add('st-bad');
            textContainer.innerHTML = `
                <span class="mw-model" style="opacity:0.7">${sel || '?'}</span>
                <span class="mw-arrow">→</span>
                <span class="mw-model mw-err">${disp || '?'}</span>
            `;
        }
    }

    // --- 4. Network & parsing ---

    // Active Fetch (to get data even if page is cached)
    async function fetchThreadData(slug) {
        try {
            const response = await fetch(`/rest/thread/${slug}`);
            if (response.ok) processText(await response.text());
        } catch (e) {}
    }

    function checkCurrentPage() {
        const path = location.pathname;
        if (path.startsWith('/search/') || path.startsWith('/collections/')) {
            const slug = path.split('/').pop();
            if (slug && slug.length > 5) setTimeout(() => fetchThreadData(slug), CONFIG.apiDelay);
        }
    }

    function deepFind(obj) {
        let res = {};
        function walk(v, depth = 0) {
            if (depth > 20 || !v || typeof v !== 'object') return;
            if (res.display_model && res.user_selected_model) return;
            if (v.display_model) res.display_model = v.display_model;
            if (v.user_selected_model) res.user_selected_model = v.user_selected_model;
            for (let k in v) walk(v[k], depth + 1);
        }
        walk(obj);
        return res;
    }

    function processText(text) {
        if (!text || typeof text !== 'string') return;
        if (!text.includes('display_model') && !text.includes('user_selected_model')) return;
        try {
            const json = JSON.parse(text);
            const found = deepFind(json);
            if (found.display_model || found.user_selected_model) {
                updateUI(found.display_model, found.user_selected_model);
                return;
            }
        } catch (e) {}
        const dm = /"display_model"\s*:\s*"([^"]+)"/.exec(text);
        const um = /"user_selected_model"\s*:\s*"([^"]+)"/.exec(text);
        if (dm || um) updateUI(dm ? dm[1] : null, um ? um[1] : null);
    }

    // Inject script into page context to hook window.fetch
    function injectInterceptor() {
        const script = document.createElement('script');
        script.textContent = `
        (function() {
            function post(t) { window.postMessage({ __mw: true, type: 'M', text: t }, '*'); }
            const origFetch = window.fetch;
            window.fetch = function(...args) {
                return origFetch.apply(this, args).then(res => {
                    try { res.clone().text().then(post).catch(()=>{}); } catch(e) {}
                    return res;
                });
            };
        })();
        `;
        (document.head || document.documentElement).appendChild(script);
        script.remove();
    }

    // --- Init ---
    function init() {
        injectStyles();
        createPill();
        injectInterceptor();

        window.addEventListener('message', (e) => {
            if (e.data?.type === 'M') processText(e.data.text);
        });

        // SPA handling: re-inject if body changes significantly
        new MutationObserver(() => {
            if (!document.getElementById('mw-pill') && document.body) createPill();
        }).observe(document, {subtree: true, childList: true});

        // URL change handling
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                updateUI(null, null);
                checkCurrentPage();
            }
        }).observe(document, {subtree: true, childList: true});

        checkCurrentPage();
    }

    if (document.body) init();
    else document.addEventListener('DOMContentLoaded', init);

})();