Greasy Fork

来自缓存

Greasy Fork is available in English.

Claude Floating Control Panel

浮动面板显示Claude的Usage和功能开关

// ==UserScript==
// @name         Claude Floating Control Panel
// @name:en         Claude Floating Control Panel
// @namespace    usage-and-quick-settings-of-claude
// @author       Yalums
// @version      1.2
// @description 浮动面板显示Claude的Usage和功能开关
// @description:en  Floating panel to display Claude usage and feature toggles
// @match        https://claude.ai/*
// @grant        none
// @run-at       document-idle
// @license      GNU General Public License v3.0
// ==/UserScript==

(function() {
    'use strict';

    const FEATURES = [
        { key: 'enabled_monkeys_in_a_barrel', name: 'Code execution', desc: 'Virtual code environment (supports archives)', exclusive: 'enabled_artifacts_attachments' },
        { key: 'enabled_artifacts_attachments', name: 'Repl Tool', desc: 'Additional features for Artifacts', exclusive: 'enabled_monkeys_in_a_barrel' },
        { key: 'enabled_saffron', name: 'Memory', desc: 'Cross-window memory' },
        { key: 'enabled_saffron_search', name: 'Search chats', desc: 'Chat search' },
        { key: 'enabled_sourdough', name: 'Projects', desc: 'Project memory' },
    ];

    let panelState = {
        isExpanded: localStorage.getItem('claudePanel_expanded') !== 'false',
        position: JSON.parse(localStorage.getItem('claudePanel_position') || '{"right":"20px","bottom":"20px"}')
    };

    async function getUserSettings() {
        try {
            const response = await fetch('/api/account', {
                credentials: 'include'
            });
            const data = await response.json();
            return data.settings;
        } catch (err) {
            console.error('[Claude Panel] Failed to fetch settings:', err);
            return null;
        }
    }

    async function toggleFeature(key, currentValue, exclusiveKey = null) {
        try {
            const body = { [key]: !currentValue };

            if (exclusiveKey && !currentValue) {
                body[exclusiveKey] = false;
            }

            const response = await fetch('/api/account/settings', {
                method: 'PATCH',
                headers: {
                    'Content-Type': 'application/json'
                },
                credentials: 'include',
                body: JSON.stringify(body)
            });

            if (!response.ok) {
                console.error('[Claude Panel] HTTP error:', response.status);
                return { success: false };
            }

            const result = await response.json();
            return { success: true, data: result };
        } catch (err) {
            console.error('[Claude Panel] Toggle failed:', err);
            return { success: false };
        }
    }

    async function getUsageData() {
        try {
            const orgsResponse = await fetch('/api/organizations', {
                credentials: 'include'
            });
            const orgs = await orgsResponse.json();
            const orgId = orgs[0]?.uuid;
            if (!orgId) return null;

            const usageResponse = await fetch(`/api/organizations/${orgId}/usage`, {
                credentials: 'include'
            });
            return await usageResponse.json();
        } catch (err) {
            console.error('[Claude Panel] Failed to fetch usage:', err);
            return null;
        }
    }

    function formatResetTime(isoTime) {
        if (!isoTime) return 'N/A';
        const date = new Date(isoTime);
        const now = new Date();
        const diff = date - now;
        const minutes = Math.floor(diff / 60000);
        const hours = Math.floor(diff / 3600000);
        const days = Math.floor(diff / 86400000);

        if (minutes < 1) return 'Resetting soon';
        if (minutes < 60) return `In ${minutes} min`;
        if (hours < 24) return `In ${hours} hr`;
        return `In ${days} days`;
    }

    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #claude-control-panel {
                position: fixed;
                z-index: 9999;
                background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
                border: 1px solid #404040;
                border-radius: 16px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                color: #ffffff;
                max-width: 400px;
                max-height: 90vh;
                overflow: hidden;
                transition: all 0.3s ease;
            }

            #claude-control-panel.collapsed {
                width: 60px !important;
                height: 60px !important;
            }

            .panel-header {
                padding: 16px;
                background: rgba(255, 255, 255, 0.05);
                border-bottom: 1px solid #404040;
                display: flex;
                align-items: center;
                justify-content: space-between;
                cursor: move;
                user-select: none;
            }

            .panel-header h3 {
                margin: 0;
                font-size: 16px;
                font-weight: 600;
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .panel-controls {
                display: flex;
                gap: 8px;
            }

            .panel-btn {
                background: rgba(255, 255, 255, 0.1);
                border: none;
                border-radius: 6px;
                width: 28px;
                height: 28px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s;
            }

            .panel-btn:hover {
                background: rgba(255, 255, 255, 0.2);
            }

            .panel-content {
                max-height: calc(90vh - 60px);
                overflow-y: auto;
                padding: 16px;
            }

            .collapsed .panel-content,
            .collapsed .panel-header h3 span {
                display: none;
            }

            .usage-section {
                background: rgba(255, 255, 255, 0.05);
                border-radius: 12px;
                padding: 16px;
                margin-bottom: 16px;
            }

            .usage-item {
                margin-bottom: 16px;
            }

            .usage-item:last-child {
                margin-bottom: 0;
            }

            .usage-label {
                display: flex;
                justify-content: space-between;
                margin-bottom: 8px;
                font-size: 13px;
            }

            .usage-label-text {
                font-weight: 500;
                color: #e0e0e0;
            }

            .usage-label-time {
                color: #909090;
                font-size: 12px;
            }

            .usage-bar {
                height: 8px;
                background: rgba(255, 255, 255, 0.1);
                border-radius: 4px;
                overflow: hidden;
                margin-bottom: 4px;
            }

            .usage-bar-fill {
                height: 100%;
                background: linear-gradient(90deg, #4ade80 0%, #22c55e 100%);
                transition: width 0.3s ease;
            }

            .usage-bar-fill.warning {
                background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%);
            }

            .usage-bar-fill.danger {
                background: linear-gradient(90deg, #f87171 0%, #ef4444 100%);
            }

            .usage-percent {
                text-align: right;
                font-size: 12px;
                color: #b0b0b0;
            }

            .features-section {
                background: rgba(255, 255, 255, 0.05);
                border-radius: 12px;
                padding: 16px;
            }

            .section-title {
                font-size: 14px;
                font-weight: 600;
                margin-bottom: 12px;
                color: #e0e0e0;
            }

            .feature-item {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 12px 0;
                border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                gap: 24px;
            }

            .feature-item:last-child {
                border-bottom: none;
            }

            .feature-info {
                flex: 1;
                min-width: 0;
            }

            .feature-name {
                font-size: 13px;
                font-weight: 500;
                color: #e0e0e0;
                margin-bottom: 4px;
            }

            .feature-desc {
                font-size: 11px;
                color: #909090;
                line-height: 1.3;
            }

            .feature-toggle {
                padding: 6px 12px;
                border-radius: 6px;
                border: none;
                font-size: 12px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.2s;
                min-width: 50px;
                flex-shrink: 0;
            }

            .feature-toggle.on {
                background: #22c55e;
                color: white;
            }

            .feature-toggle.off {
                background: rgba(255, 255, 255, 0.1);
                color: #909090;
            }

            .feature-toggle:hover:not(:disabled) {
                transform: scale(1.05);
            }

            .feature-toggle:disabled {
                opacity: 0.5;
                cursor: not-allowed;
            }

            .refresh-btn {
                width: 100%;
                padding: 10px;
                background: rgba(255, 255, 255, 0.1);
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 8px;
                color: #e0e0e0;
                font-size: 13px;
                cursor: pointer;
                margin-top: 16px;
                transition: all 0.2s;
            }

            .refresh-btn:hover {
                background: rgba(255, 255, 255, 0.15);
            }

            .loading {
                text-align: center;
                padding: 20px;
                color: #909090;
                font-size: 13px;
            }

            .panel-content::-webkit-scrollbar {
                width: 6px;
            }

            .panel-content::-webkit-scrollbar-track {
                background: rgba(255, 255, 255, 0.05);
            }

            .panel-content::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.2);
                border-radius: 3px;
            }

            .panel-content::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.3);
            }
        `;
        document.head.appendChild(style);
    }

    function createPanel() {
        const panel = document.createElement('div');
        panel.id = 'claude-control-panel';
        panel.style.right = panelState.position.right || '20px';
        panel.style.bottom = panelState.position.bottom || '20px';

        if (!panelState.isExpanded) {
            panel.classList.add('collapsed');
        }

        panel.innerHTML = `
            <div class="panel-header">
                <h3>
                    <span>⚡</span>
                    <span>Claude Control Panel</span>
                </h3>
                <div class="panel-controls">
                    <button class="panel-btn" id="toggle-panel" title="Collapse/Expand">
                        ${panelState.isExpanded ? '−' : '+'}
                    </button>
                </div>
            </div>
            <div class="panel-content">
                <div class="loading">Loading...</div>
            </div>
        `;

        document.body.appendChild(panel);
        makeDraggable(panel);

        panel.querySelector('#toggle-panel').addEventListener('click', () => {
            panelState.isExpanded = !panelState.isExpanded;
            localStorage.setItem('claudePanel_expanded', panelState.isExpanded);
            panel.classList.toggle('collapsed');
            panel.querySelector('#toggle-panel').textContent = panelState.isExpanded ? '−' : '+';
        });

        return panel;
    }

    async function updatePanelContent(panel) {
        const content = panel.querySelector('.panel-content');

        const [usageData, settings] = await Promise.all([
            getUsageData(),
            getUserSettings()
        ]);

        if (!usageData && !settings) {
            content.innerHTML = '<div class="loading">❌ Failed to load</div>';
            return;
        }

        let html = '';

        if (usageData) {
            html += '<div class="usage-section"><div class="section-title">📊 Usage</div>';

            if (usageData.five_hour) {
                const percent = usageData.five_hour.utilization || 0;
                const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
                html += `
                    <div class="usage-item">
                        <div class="usage-label">
                            <span class="usage-label-text">Current Session</span>
                            <span class="usage-label-time">${formatResetTime(usageData.five_hour.resets_at)}</span>
                        </div>
                        <div class="usage-bar">
                            <div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
                        </div>
                        <div class="usage-percent">${percent}% used</div>
                    </div>
                `;
            }

            if (usageData.seven_day) {
                const percent = usageData.seven_day.utilization || 0;
                const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
                html += `
                    <div class="usage-item">
                        <div class="usage-label">
                            <span class="usage-label-text">Weekly Limit (All Models)</span>
                            <span class="usage-label-time">${formatResetTime(usageData.seven_day.resets_at)}</span>
                        </div>
                        <div class="usage-bar">
                            <div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
                        </div>
                        <div class="usage-percent">${percent}% used</div>
                    </div>
                `;
            }

            if (usageData.seven_day_opus) {
                const percent = usageData.seven_day_opus.utilization || 0;
                const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
                html += `
                    <div class="usage-item">
                        <div class="usage-label">
                            <span class="usage-label-text">Weekly Limit (Opus Only)</span>
                            <span class="usage-label-time">${formatResetTime(usageData.seven_day_opus.resets_at)}</span>
                        </div>
                        <div class="usage-bar">
                            <div class="usage-bar-fill ${barClass}" style="width: ${percent}%"></div>
                        </div>
                        <div class="usage-percent">${percent}% used</div>
                    </div>
                `;
            }

            html += '</div>';
        }

        if (settings) {
            html += '<div class="features-section"><div class="section-title">🔧 Feature Toggles</div>';

            FEATURES.forEach(feature => {
                const isEnabled = settings[feature.key] === true;
                html += `
                    <div class="feature-item">
                        <div class="feature-info">
                            <div class="feature-name">${feature.name}</div>
                            <div class="feature-desc">${feature.desc}</div>
                        </div>
                        <button class="feature-toggle ${isEnabled ? 'on' : 'off'}"
                                data-key="${feature.key}"
                                data-value="${isEnabled}"
                                data-exclusive="${feature.exclusive || ''}">
                            ${isEnabled ? '✓ ON' : 'OFF'}
                        </button>
                    </div>
                `;
            });

            html += '</div>';
        }

        html += '<button class="refresh-btn" id="refresh-panel">🔄 Refresh Data</button>';

        content.innerHTML = html;

        content.querySelectorAll('.feature-toggle').forEach(btn => {
            btn.addEventListener('click', async (e) => {
                const button = e.target;
                const key = button.dataset.key;
                const currentValue = button.dataset.value === 'true';
                const exclusiveKey = button.dataset.exclusive || null;

                button.disabled = true;
                button.textContent = '...';

                const result = await toggleFeature(key, currentValue, exclusiveKey);

                setTimeout(() => updatePanelContent(panel), 300);
            });
        });

        content.querySelector('#refresh-panel')?.addEventListener('click', () => {
            content.querySelector('.loading')?.remove();
            const loading = document.createElement('div');
            loading.className = 'loading';
            loading.textContent = 'Refreshing...';
            content.insertBefore(loading, content.firstChild);

            setTimeout(() => {
                loading.remove();
                updatePanelContent(panel);
            }, 300);
        });
    }

    function makeDraggable(element) {
        const header = element.querySelector('.panel-header');
        let isDragging = false;
        let currentX, currentY, initialX, initialY;

        header.addEventListener('mousedown', (e) => {
            if (e.target.closest('.panel-btn')) return;

            isDragging = true;
            initialX = e.clientX - element.offsetLeft;
            initialY = e.clientY - element.offsetTop;
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            e.preventDefault();
            currentX = e.clientX - initialX;
            currentY = e.clientY - initialY;

            element.style.left = currentX + 'px';
            element.style.top = currentY + 'px';
            element.style.right = 'auto';
            element.style.bottom = 'auto';
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                panelState.position = {
                    left: element.style.left,
                    top: element.style.top
                };
                localStorage.setItem('claudePanel_position', JSON.stringify(panelState.position));
            }
        });
    }

    function init() {
        injectStyles();
        const panel = createPanel();
        updatePanelContent(panel);

        setInterval(() => updatePanelContent(panel), 60000);
    }

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