Greasy Fork is available in English.

YOS Hub

YOS Hub universel v3.1 - Point d'entrée unique pour toutes les fonctionnalités YOS.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         YOS Hub
// @version      3.1.0
// @description  YOS Hub universel v3.1 - Point d'entrée unique pour toutes les fonctionnalités YOS.
// @author       Manus
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @match        https://claude.ai/*
// @match        https://gemini.google.com/*
// @match        https://www.perplexity.ai/*
// @match        https://perplexity.ai/*
// @match        https://manus.im/*
// @namespace    https://github.com/yj000018/yos-scripts
// @supportURL   https://github.com/yj000018/yos-scripts/issues
// @run-at       document-idle
// @run-in       both
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_setClipboard
// ==/UserScript==

(function() {
    'use strict';

    const YOS_API_ENDPOINT = 'https://yos-archiver-endpoint.fly.dev';
    const YOS_API_KEY = 'yos-4a43cb42f754b233a3fc458e3213ddcfc6805454';
    const NOTION_BASE_YOS_ARCHIVES_ID = '31235e21-8cf8-8126-9212-f5a0eebadce0';
    const PAGE_YOS_PARENT_ID = '30535e21-8cf8-8014-acb7-e40e2938f89e';
    const DEFAULT_EMAIL = '[email protected]';

    // Design colors
    const COLOR_DARK_BG = '#0d0d0f';
    const COLOR_PANEL_BG = '#141418';
    const COLOR_ACCENT_PURPLE = '#7c3aed';
    const COLOR_LIGHT_PURPLE = '#a78bfa';
    const COLOR_WHITE_TEXT = '#ffffff';

    // Platform detection selectors
    const PLATFORM_SELECTORS = {
        'chat.openai.com': { name: 'ChatGPT', selector: '.text-token-text-primary' },
        'claude.ai': { name: 'Claude', selector: '.font-claude-message' },
        'gemini.google.com': { name: 'Gemini', selector: '.model-response-text' },
        'perplexity.ai': { name: 'Perplexity', selector: '.prose' },
        'manus.im': { name: 'Manus', selector: '.message-content' }
    };

    // Add global styles
    GM_addStyle(`
        body {
            background-color: ${COLOR_DARK_BG} !important;
        }
        .yos-fab {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            background-color: ${COLOR_ACCENT_PURPLE};
            color: ${COLOR_WHITE_TEXT};
            font-size: 24px;
            display: flex;
            justify-content: center;
            align-items: center;
            cursor: pointer;
            z-index: 10000;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
            transition: background-color 0.3s ease;
        }
        .yos-fab:hover {
            background-color: ${COLOR_LIGHT_PURPLE};
        }
        .yos-panel {
            position: fixed;
            bottom: 80px; /* Above FAB */
            right: 20px;
            width: 340px;
            max-height: 80vh;
            overflow-y: auto;
            background-color: ${COLOR_PANEL_BG};
            color: ${COLOR_WHITE_TEXT};
            border-radius: 12px;
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
            z-index: 9999;
            display: none;
            flex-direction: column;
            padding: 15px;
            box-sizing: border-box;
        }
        .yos-panel.open {
            display: flex;
        }
        .yos-panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #333;
        }
        .yos-panel-header h3 {
            margin: 0;
            font-size: 18px;
            color: ${COLOR_WHITE_TEXT};
            display: flex;
            align-items: center;
        }
        .yos-panel-header .yos-logo {
            font-size: 24px;
            font-weight: bold;
            color: ${COLOR_ACCENT_PURPLE};
            margin-right: 8px;
        }
        .yos-panel-header .yos-source {
            font-size: 12px;
            color: #aaa;
            margin-left: 10px;
        }
        .yos-close-btn {
            background: none;
            border: none;
            color: ${COLOR_WHITE_TEXT};
            font-size: 20px;
            cursor: pointer;
        }
        .yos-search-bar {
            width: 100%;
            padding: 10px;
            margin-bottom: 15px;
            border: 1px solid #333;
            border-radius: 8px;
            background-color: #222;
            color: ${COLOR_WHITE_TEXT};
            box-sizing: border-box;
        }
        .yos-category-accordion {
            margin-bottom: 10px;
            border: 1px solid #333;
            border-radius: 8px;
            overflow: hidden;
        }
        .yos-category-header {
            background-color: #2a2a2a;
            padding: 12px 15px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-weight: bold;
            color: ${COLOR_WHITE_TEXT};
        }
        .yos-category-header:hover {
            background-color: #3a3a3a;
        }
        .yos-category-header .yos-icon {
            margin-right: 10px;
        }
        .yos-category-content {
            padding: 0 15px;
            background-color: ${COLOR_PANEL_BG};
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease-out;
        }
        .yos-category-content.open {
            max-height: 500px; /* Adjust as needed */
            padding: 10px 15px;
        }
        .yos-action-item {
            padding: 8px 0;
            cursor: pointer;
            color: #ccc;
            transition: color 0.2s ease;
        }
        .yos-action-item:hover {
            color: ${COLOR_ACCENT_PURPLE};
        }
        .yos-spinner {
            border: 4px solid rgba(255, 255, 255, 0.3);
            border-top: 4px solid ${COLOR_ACCENT_PURPLE};
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            margin-left: 10px;
            display: none;
        }
        .yos-spinner.active {
            display: inline-block;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .yos-toast {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(0, 0, 0, 0.8);
            color: ${COLOR_WHITE_TEXT};
            padding: 10px 20px;
            border-radius: 8px;
            z-index: 10001;
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
        }
        .yos-toast.show {
            opacity: 1;
        }
        .yos-badge {
            display: inline-block;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 10px;
            font-weight: bold;
            margin-left: 5px;
            color: ${COLOR_WHITE_TEXT};
        }
        .yos-badge.yos {
            background-color: ${COLOR_ACCENT_PURPLE};
        }
        .yos-badge.archive {
            background-color: #666;
        }
        .yos-badge.deleted {
            background-color: #dc3545;
            text-decoration: line-through;
        }
    `);

    function getPlatformInfo() {
        const hostname = window.location.hostname;
        for (const key in PLATFORM_SELECTORS) {
            if (hostname.includes(key)) {
                return PLATFORM_SELECTORS[key];
            }
        }
        return { name: 'Unknown', selector: null };
    }

    function extractConversationContent(selector) {
        if (!selector) return 'No content selector for this platform.';
        const elements = document.querySelectorAll(selector);
        let content = '';
        elements.forEach(el => {
            content += el.innerText + '\n\n';
        });
        return content.trim();
    }

    async function callYOSApi(action, data) {
        const spinner = document.querySelector('.yos-spinner');
        spinner.classList.add('active');
        try {
            const response = await fetch(`${YOS_API_ENDPOINT}/${action}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-YOS-API-KEY': YOS_API_KEY
                },
                body: JSON.stringify(data)
            });
            const result = await response.json();
            if (!response.ok) {
                throw new Error(result.message || 'API call failed');
            }
            showToast('Action YOS réussie !', 'success');
            return result;
        } catch (error) {
            console.error('YOS API Error:', error);
            showToast(`Erreur YOS: ${error.message}`, 'error');
            return null;
        } finally {
            spinner.classList.remove('active');
        }
    }

    function showToast(message, type = 'info') {
        let toast = document.querySelector('.yos-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.classList.add('yos-toast');
            document.body.appendChild(toast);
        }
        toast.textContent = message;
        toast.style.backgroundColor = type === 'success' ? '#28a745' : (type === 'error' ? '#dc3545' : 'rgba(0,0,0,0.8)');
        toast.classList.add('show');
        setTimeout(() => {
            toast.classList.remove('show');
        }, 3000);
    }

    function copyToClipboard(text) {
        GM_setClipboard(text);
        showToast('Commande copiée dans le presse-papier !', 'info');
    }

    function createFAB() {
        const fab = document.createElement('div');
        fab.classList.add('yos-fab');
        fab.textContent = 'Y';
        document.body.appendChild(fab);

        fab.addEventListener('click', () => {
            const panel = document.querySelector('.yos-panel');
            panel.classList.toggle('open');
        });
    }

    function createPanel() {
        const panel = document.createElement('div');
        panel.classList.add('yos-panel');

        const platformInfo = getPlatformInfo();

        panel.innerHTML = `
            <div class="yos-panel-header">
                <h3><span class="yos-logo">Y</span> YOS Hub <span class="yos-source">(${platformInfo.name})</span></h3>
                <div class="yos-spinner"></div>
                <button class="yos-close-btn">&times;</button>
            </div>
            <input type="text" class="yos-search-bar" placeholder="Rechercher des actions...">
            <div class="yos-categories">
                <!-- Categories will be injected here -->
            </div>
        `;
        document.body.appendChild(panel);

        panel.querySelector('.yos-close-btn').addEventListener('click', () => {
            panel.classList.remove('open');
        });

        const categoriesContainer = panel.querySelector('.yos-categories');
        const searchBar = panel.querySelector('.yos-search-bar');

        const categories = [
            { name: 'MÉMOIRE', icon: '🧠', actions: [
                { name: 'Push to YOS', command: 'y.mem.push', apiAction: 'push' },
                { name: 'Archive', command: 'y.mem.archive', apiAction: 'archive' },
                { name: 'Push + Archive', command: 'y.mem.push_archive', apiAction: 'push_archive' },
                { name: 'Rechercher', command: 'y.mem.search' }
            ]},
            { name: 'SÉCURITÉ', icon: '🔐', actions: [
                { name: 'Login', command: 'y.sec.login' },
                { name: 'Vérifier accès', command: 'y.sec.check_access' }
            ]},
            { name: 'RECHERCHE', icon: '🔍', actions: [
                { name: 'Web', command: 'y.search.web' },
                { name: 'Analyser page', command: 'y.search.analyze_page' }
            ]},
            { name: 'CONTENU', icon: '✍️', actions: [
                { name: 'Résumer', command: 'y.content.summarize' },
                { name: 'Extraire', command: 'y.content.extract' }
            ]},
            { name: 'GESTION', icon: '📋', actions: [
                { name: 'Créer tâche', command: 'y.manage.create_task' },
                { name: 'Projet', command: 'y.manage.project' }
            ]},
            { name: 'SYSTÈME', icon: '⚙️', actions: [
                { name: 'Aide', command: 'y.sys.help' },
                { name: 'Statut', command: 'y.sys.status' }
            ]}
        ];

        function renderCategories(filter = '') {
            categoriesContainer.innerHTML = '';
            categories.forEach(category => {
                const filteredActions = category.actions.filter(action =>
                    action.name.toLowerCase().includes(filter.toLowerCase()) ||
                    action.command.toLowerCase().includes(filter.toLowerCase())
                );

                if (filteredActions.length > 0) {
                    const categoryDiv = document.createElement('div');
                    categoryDiv.classList.add('yos-category-accordion');
                    categoryDiv.innerHTML = `
                        <div class="yos-category-header">
                            <div><span class="yos-icon">${category.icon}</span>${category.name}</div>
                            <span>&#9660;</span>
                        </div>
                        <div class="yos-category-content">
                            <!-- Actions will be injected here -->
                        </div>
                    `;
                    categoriesContainer.appendChild(categoryDiv);

                    const header = categoryDiv.querySelector('.yos-category-header');
                    const content = categoryDiv.querySelector('.yos-category-content');
                    const arrow = header.querySelector('span');

                    header.addEventListener('click', () => {
                        content.classList.toggle('open');
                        arrow.innerHTML = content.classList.contains('open') ? '&#9650;' : '&#9660;';
                    });

                    filteredActions.forEach(action => {
                        const actionItem = document.createElement('div');
                        actionItem.classList.add('yos-action-item');
                        actionItem.textContent = action.name;
                        actionItem.addEventListener('click', async () => {
                            if (category.name === 'MÉMOIRE' && action.apiAction) {
                                const conversationContent = extractConversationContent(platformInfo.selector);
                                if (conversationContent) {
                                    const payload = {
                                        content: conversationContent,
                                        platform: platformInfo.name,
                                        url: window.location.href,
                                        notionBaseId: NOTION_BASE_YOS_ARCHIVES_ID,
                                        notionParentId: PAGE_YOS_PARENT_ID,
                                        email: DEFAULT_EMAIL
                                    };
                                    const result = await callYOSApi(action.apiAction, payload);
                                    if (result) {
                                        // Handle badge injection for sidebar (conceptual, requires specific DOM knowledge of each platform)
                                        // For demonstration, we'll just log and show a toast
                                        console.log(`YOS Memory action '${action.name}' successful. Result:`, result);
                                        showToast(`YOS Memory: ${action.name} completed.`, 'success');
                                        // Example of setting a badge (this part is highly dependent on target site's DOM)
                                        // GM_setValue('yos_badge_status_' + window.location.href, 'yos');
                                        // injectBadgeToSidebar(); // This function would need to be implemented per platform
                                    }
                                } else {
                                    showToast('Impossible d\'extraire le contenu de la conversation.', 'error');
                                }
                            } else {
                                copyToClipboard(action.command);
                                showToast(`Copié → Ouvre Manus et tape: ${action.command}`, 'info');
                            }
                        });
                        content.appendChild(actionItem);
                    });
                }
            });
        }

        searchBar.addEventListener('input', (e) => {
            renderCategories(e.target.value);
        });

        renderCategories(); // Initial render
    }

    // Function to inject badges (conceptual, highly platform-dependent)
    function injectBadgeToSidebar() {
        // This is a placeholder. Actual implementation would require specific DOM manipulation
        // for each platform (ChatGPT, Claude, Gemini, etc.) to find their sidebar elements
        // and inject a badge. It would also need to read GM_getValue for persistence.
        console.log('Attempting to inject YOS badge to sidebar...');
        // Example: Find a common sidebar element and append a badge
        // const sidebar = document.querySelector('.sidebar-selector-for-platform');
        // if (sidebar) {
        //     const badgeStatus = GM_getValue('yos_badge_status_' + window.location.href, null);
        //     if (badgeStatus) {
        //         const badge = document.createElement('span');
        //         badge.classList.add('yos-badge', badgeStatus);
        //         badge.textContent = badgeStatus.toUpperCase();
        //         sidebar.appendChild(badge);
        //     }
        // }
    }

    // Initialize YOS Hub
    function initYOSHub() {
        // Avoid double injection
        if (document.getElementById('yos-fab')) return;
        createFAB();
        createPanel();
        injectBadgeToSidebar();
    }

    // SPA-aware initialization with retry
    // Manus, ChatGPT, Claude are SPAs — DOM loads async after page load
    function waitForBodyAndInit() {
        if (document.body) {
            initYOSHub();
        } else {
            setTimeout(waitForBodyAndInit, 100);
        }
    }

    // Handle SPA navigation (pushState / replaceState)
    function patchHistory() {
        const _push = history.pushState.bind(history);
        const _replace = history.replaceState.bind(history);
        history.pushState = function() {
            _push.apply(history, arguments);
            setTimeout(initYOSHub, 500); // Re-inject after SPA navigation
        };
        history.replaceState = function() {
            _replace.apply(history, arguments);
            setTimeout(initYOSHub, 500);
        };
        window.addEventListener('popstate', function() {
            setTimeout(initYOSHub, 500);
        });
    }

    // MutationObserver fallback — watches for DOM changes (React hydration)
    function observeAndInit() {
        const observer = new MutationObserver(function(mutations) {
            if (!document.getElementById('yos-fab') && document.body) {
                initYOSHub();
            }
        });
        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: false });
        }
    }

    // Boot sequence
    patchHistory();

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
            waitForBodyAndInit();
            observeAndInit();
        });
    } else {
        waitForBodyAndInit();
        observeAndInit();
    }

    // Hard retry after 2s and 5s as final fallback
    setTimeout(initYOSHub, 2000);
    setTimeout(initYOSHub, 5000);

})();