DeepSeek Playground UI

全屏双栏界面:左栏输入区(恒定编辑状态),右栏输出区。Enter 换行,Ctrl+Enter 发送。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         DeepSeek Playground UI
// @namespace    http://tampermonkey.net/
// @version      1.5.3
// @description  全屏双栏界面:左栏输入区(恒定编辑状态),右栏输出区。Enter 换行,Ctrl+Enter 发送。 
// @description:zh-CN 全屏双栏界面:左栏输入区(恒定编辑状态),右栏输出区。Enter 换行,Ctrl+Enter 发送。 
// @description:zh-TW 全螢幕雙欄界面:左欄輸入區(恆定編輯狀態),右欄輸出區。Enter 換行,Ctrl+Enter 傳送。 
// @description:en Fullscreen dual-panel UI: left panel for input (always editable), right panel for output. Enter for newline, Ctrl+Enter to send. 
// @match        https://chat.deepseek.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepseek.com
// @grant        GM_addStyle
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY = 'ds_playground_ui';
    let isPlaygroundUI = localStorage.getItem(STORAGE_KEY) === 'true';
    let lastEditClickTime = 0;

    // 样式注入
    GM_addStyle(`
        /* 切换按钮样式 */
        #ds-playground-toggle {
            position: fixed;
            top: 12px;
            right: 140px;
            z-index: 9999;
            padding: 6px 12px;
            background-color: var(--dsw-alias-bg-layer-2, #ffffff);
            border: 1px solid var(--dsw-alias-brand-primary, #4d6bfe);
            color: var(--dsw-alias-brand-primary, #4d6bfe);
            border-radius: 8px;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            transition: all 0.2s;
            box-shadow: 0 2px 6px rgba(0,0,0,0.05);
        }
        #ds-playground-toggle:hover, #ds-playground-toggle.active {
            background-color: var(--dsw-alias-brand-primary, #4d6bfe);
            color: #fff;
        }

        /* 锁定页面滚动与高度 */
        body.playground-mode, body.playground-mode #root {
            overflow: hidden !important;
            height: 100% !important;
        }
        body.playground-mode .ds-virtual-list {
            height: calc(100vh - 60px) !important;
            overflow: hidden !important;
        }
        body.playground-mode .ds-virtual-list-items {
            min-height: 96% !important;
            height: 96% !important;
            padding: 0 !important;
        }
        body.playground-mode .ds-virtual-list-visible-items {
            position: relative !important;
            transform: none !important;
            height: 96% !important;
            min-height: 96% !important;
        }

        /* 左右双面板使用绝对定位,避免 React 动态尺寸干扰 */
        /* 左栏:用户输入区 */
        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            width: 50% !important;
            height: 100% !important;
            padding: 24px 12px 24px 24px !important;
            box-sizing: border-box !important;
            display: flex !important;
            flex-direction: column !important;
        }
        /* 右栏:模型输出区 */
        body.playground-mode [data-virtual-list-item-key]:nth-of-type(2) {
            position: absolute !important;
            top: 0 !important;
            right: 0 !important;
            left: 50% !important;
            width: 50% !important;
            height: 100% !important;
            padding: 24px 24px 24px 12px !important;
            box-sizing: border-box !important;
            display: flex !important;
            flex-direction: column !important;
        }

        /* 隐藏第一个和第二个以外的所有列表项 */
        body.playground-mode [data-virtual-list-item-key]:not(:nth-of-type(1)):not(:nth-of-type(2)) {
            display: none !important;
        }

        /* 左栏内部布局调整 */
        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1):has(.ds-textarea) > .ds-message {
            display: none !important;
        }

        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) > div:not(.ds-message) {
            flex: 1 1 auto !important;
            height: 100% !important;
            max-height: none !important;
            display: flex !important;
            flex-direction: column !important;
        }

        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) .ds-textarea {
            flex: 1 1 auto !important;
            height: 100% !important;
            display: flex !important;
            flex-direction: column !important;
            max-width: none !important;
            border-radius: 12px !important;
        }

        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) .ds-textarea > div:has(textarea) {
            flex: 1 1 auto !important;
            height: 100% !important;
            max-height: none !important;
            position: relative !important;
        }

        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) textarea[name="user query"] {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            width: 100% !important;
            height: 100% !important;
            max-height: none !important;
            resize: none !important;
            overflow-y: auto !important;
        }

        /* 阻止镜像元素影响布局 */
        body.playground-mode [data-virtual-list-item-key]:nth-of-type(1) .ds-textarea__mirror {
            display: none !important;
        }

        /* 右栏独立滚动条 */
        body.playground-mode [data-virtual-list-item-key]:nth-of-type(2) > .ds-message {
            flex: 1 1 auto !important;
            height: 100% !important;
            overflow-y: auto !important;
            background: var(--dsw-alias-bg-layer-1, #f9f9f9) !important;
            border-radius: 12px !important;
            padding: 24px !important;
            border: 1px solid var(--dsw-alias-border-1, #eaeaea) !important;
            max-width: none !important;
            margin: 0 !important;
        }

        /* 隐藏页面底部的输入框区域 */
        body.playground-mode div:has(> textarea[name="search"]) {
            display: none !important;
        }
    `);

    // 检查是否存在至少一轮对话
    function hasConversation() {
        // 通过是否存在消息组件判断是否有对话记录
        const messages = document.querySelectorAll('.ds-message');
        return messages.length > 0;
    }

    // 切换模式时的检查与状态同步
    function setPlaygroundUIState(enable) {
        if (enable && !hasConversation()) {
            alert("请先进行一轮对话。Playground UI 只能从已有的对话切入");
            return false;
        }
        if (isPlaygroundUI !== enable) {
            isPlaygroundUI = enable;
            localStorage.setItem(STORAGE_KEY, isPlaygroundUI);
            updateToggleButton();
            applyUI();
        }
        return true;
    }

    function updateToggleButton() {
        const btn = document.getElementById('ds-playground-toggle');
        if (btn) {
            btn.innerHTML = isPlaygroundUI ? '🛠️ Playground UI: ON' : '🛠️ Playground UI: OFF';
            btn.className = isPlaygroundUI ? 'active' : '';
        }
    }

    function applyUI() {
        if (isPlaygroundUI) {
            document.body.classList.add('playground-mode');
            forceEditMode();
        } else {
            document.body.classList.remove('playground-mode');
            lastEditClickTime = 0;
        }
    }

    function initToggleUI() {
        if (document.getElementById('ds-playground-toggle')) return;
        const btn = document.createElement('button');
        btn.id = 'ds-playground-toggle';
        btn.innerHTML = isPlaygroundUI ? '🛠️ Playground UI: ON' : '🛠️ Playground UI: OFF';
        btn.className = isPlaygroundUI ? 'active' : '';

        btn.addEventListener('click', () => {
            // 尝试切换状态,若不允许则保持原状态
            const newState = !isPlaygroundUI;
            if (newState && !hasConversation()) {
                alert("请先进行一轮对话。Playground UI 只能从已有的对话切入");
                return;
            }
            isPlaygroundUI = newState;
            localStorage.setItem(STORAGE_KEY, isPlaygroundUI);
            updateToggleButton();
            applyUI();
        });

        document.body.appendChild(btn);
        applyUI();
    }

    // 强制进入编辑模式(点击铅笔图标)
    function forceEditMode() {
        if (!isPlaygroundUI) return;

        const leftPanel = document.querySelector('[data-virtual-list-item-key]:nth-of-type(1)');
        if (!leftPanel) return;

        const isEditing = leftPanel.querySelector('textarea[name="user query"]');
        if (isEditing) return;

        // 防抖:避免短时间内重复点击
        if (Date.now() - lastEditClickTime < 1000) return;

        const svgs = leftPanel.querySelectorAll('svg');
        for (let svg of svgs) {
            // 依据 SVG 路径特征定位编辑按钮
            if (svg.innerHTML.includes('9.94076')) {
                const editBtn = svg.closest('[role="button"]');
                if (editBtn) {
                    lastEditClickTime = Date.now();
                    editBtn.click();
                    break;
                }
            }
        }
    }

    // 在文本框中插入内容
    function insertTextAtCursor(textarea, text) {
        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const textBefore = textarea.value.substring(0, start);
        const textAfter = textarea.value.substring(end, textarea.value.length);

        nativeSetter.call(textarea, textBefore + text + textAfter);
        textarea.selectionStart = textarea.selectionEnd = start + text.length;
        textarea.dispatchEvent(new Event('input', { bubbles: true }));
    }

    // 触发发送消息
    function triggerSend(textarea) {
        const container = textarea.closest('[data-virtual-list-item-key]:nth-of-type(1)');
        if (!container) return;

        const spans = container.querySelectorAll('span');
        for (let span of spans) {
            if (span.textContent.trim() === '发送') {
                const sendBtn = span.closest('[role="button"]');
                if (sendBtn) {
                    sendBtn.click();
                    break;
                }
            }
        }
    }

    // 键盘事件处理
    document.addEventListener('keydown', (e) => {
        if (!isPlaygroundUI) return;

        const target = e.target;
        if (target.tagName === 'TEXTAREA' && target.name === 'user query') {
            // Enter 换行,不发送
            if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
                e.preventDefault();
                e.stopPropagation();
                insertTextAtCursor(target, '\n');
            }
            // Ctrl+Enter 发送
            if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
                e.preventDefault();
                e.stopPropagation();
                triggerSend(target);
            }
        }
    }, true);

    // 监听 DOM 变化,保持编辑模式
    const observer = new MutationObserver(() => {
        if (isPlaygroundUI) {
            requestAnimationFrame(() => forceEditMode());
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // 启动 UI
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        initToggleUI();
        // 若初始状态为 true 但无对话,则重置为 false
        if (isPlaygroundUI && !hasConversation()) {
            isPlaygroundUI = false;
            localStorage.setItem(STORAGE_KEY, false);
            updateToggleButton();
            applyUI();
            alert("请先进行一轮对话。Playground UI 只能从已有的对话切入");
        }
    } else {
        window.addEventListener('DOMContentLoaded', () => {
            initToggleUI();
            if (isPlaygroundUI && !hasConversation()) {
                isPlaygroundUI = false;
                localStorage.setItem(STORAGE_KEY, false);
                updateToggleButton();
                applyUI();
                alert("请先进行一轮对话。Playground UI 只能从已有的对话切入");
            }
        });
    }
})();