Ask Deepseek

Registers "Ask Deepseek" in the native browser context menu (via Tampermonkey) and adds the Ctrl+Y keyboard shortcut: saves the selected text, opens chat.deepseek.com in a new tab and automatically sends the question with web search enabled.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Ask Deepseek
// @namespace    https://chat.deepseek.com/
// @version      1.3.3
// @description  Registers "Ask Deepseek" in the native browser context menu (via Tampermonkey) and adds the Ctrl+Y keyboard shortcut: saves the selected text, opens chat.deepseek.com in a new tab and automatically sends the question with web search enabled.
// @match        http://*/*
// @match        https://*/*
// @match        https://chat.deepseek.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const DEBUG = true;
  const STORAGE_KEY = 'pendingQuery';
  const DEEPSEEK_HOST = 'chat.deepseek.com';
  const UI_TIMEOUT_MS = 15000;

  const log = (...a) => console.log('[Ask Deepseek]', ...a);
  const warn = (...a) => console.warn('[Ask Deepseek]', ...a);

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  /* ------------------------------------------------------------------ */
  /*  DeepSeek UI selectors (multiple fallbacks)                        */
  /* ------------------------------------------------------------------ */
  const INPUT_SELECTORS = [
    'textarea#chat-input',
    'textarea[data-testid="chat-input"]',
    'textarea[data-testid="chat_input"]',
    'div[contenteditable="true"][role="textbox"]',
    'textarea[placeholder*="Message" i]',
    'div[contenteditable="true"]',
    'textarea[placeholder]',
  ];

  const SEND_SELECTORS = [
    'div[role="button"][aria-label="Send"]',
    'button[aria-label="Send"]',
    'div[aria-label="Send"]',
    'button[data-testid="send-button"]',
    'div[role="button"][class*="send" i]',
  ];

  const WEBSEARCH_TOGGLE_SELECTORS = [
    'button[aria-label*="Web search" i]',
    '[aria-label*="Web search" i]',
    '[title*="Web search" i]',
    '[data-testid*="web-search" i]',
    '[data-testid*="web_search" i]',
    '[data-testid*="websearch" i]',
    'button[class*="web-search" i]',
    'button[class*="web_search" i]',
    '[class*="search-toggle" i]',
  ];

  const MODE_DROPDOWN_SELECTORS = [
    'button[aria-label*="search mode" i]',
    '[data-testid*="search-mode" i]',
    '[data-testid*="search-mode-button" i]',
    'button[class*="search-mode" i]',
  ];

  const MODE_ITEM_TEXT = ['Web search'];
  const MODE_ITEM_SELECTORS = [
    '[role="menuitem"]',
    '[role="option"]',
    'div[class*="menu" i] div[class*="item" i]',
    'li[class*="item" i]',
  ];

  function queryFirst(selectors) {
    for (const sel of selectors) {
      try {
        const el = document.querySelector(sel);
        if (el) return { el, selector: sel };
      } catch (err) {
        warn('Invalid selector:', sel, err);
      }
    }
    return null;
  }

  /* ------------------------------------------------------------------ */
  /*  PART A - All pages: native context menu entry                     */
  /*  The entry is registered natively via GM_registerMenuCommand:      */
  /*  Tampermonkey (4.14+) adds it to the browser's native context      */
  /*  menu. On Firefox, an element injected into the DOM cannot         */
  /*  overlay the native menu, so this is the only robust way.          */
  /*  The entry is always present in the menu: if there is no           */
  /*  selection, clicking it does nothing.                              */
  /* ------------------------------------------------------------------ */
  const SELECTION_FRESHNESS_MS = 10000;
  let lastSelection = null;

  /* Keyboard shortcut (works when the page has focus):
     Ctrl+Y. To change it, modify the values below. */
  const SHORTCUT_KEY = 'KeyY';
  const SHORTCUT_CTRL = true;
  const SHORTCUT_SHIFT = false;

  function partA() {
    document.addEventListener('contextmenu', onContextMenu, true);
    document.addEventListener('keydown', onKeyDown, true);
    if (typeof GM_registerMenuCommand === 'function') {
      GM_registerMenuCommand('\u{1F50D} Ask Deepseek', onMenuClick);
      log('Native context menu entry registered');
    } else {
      warn('GM_registerMenuCommand not available: update Tampermonkey (4.14+)');
    }
  }

  function onKeyDown(e) {
    const modsOk = (SHORTCUT_CTRL === !!e.ctrlKey) && (SHORTCUT_SHIFT === !!e.shiftKey) && !e.altKey && !e.metaKey;
    if (!modsOk || e.code !== SHORTCUT_KEY) return;
    e.preventDefault();
    e.stopPropagation();
    const raw = window.getSelection ? window.getSelection().toString() : '';
    const clean = raw.replace(/\s+/g, ' ').trim();
    if (!clean) {
      log('Shortcut pressed with no selection: no action');
      return;
    }
    log('Shortcut: selection detected (' + clean.length + ' chars)');
    openDeepSeek(clean);
  }

  function onContextMenu() {
    lastSelection = {
      text: window.getSelection ? window.getSelection().toString() : '',
      time: Date.now(),
    };
  }

  function onMenuClick() {
    let raw = '';
    if (lastSelection && Date.now() - lastSelection.time < SELECTION_FRESHNESS_MS) {
      raw = lastSelection.text;
    } else if (window.getSelection) {
      raw = window.getSelection().toString();
    }
    const clean = raw.replace(/\s+/g, ' ').trim();
    if (!clean) {
      log('No selection: no action');
      return;
    }
    log('Selection detected (' + clean.length + ' chars)');
    openDeepSeek(clean);
  }

  function openDeepSeek(text) {
    GM_setValue(STORAGE_KEY, text);
    log('Question saved (' + text.length + ' chars)');
    window.open('https://chat.deepseek.com/', '_blank', 'noopener');
  }

  /* ------------------------------------------------------------------ */
  /*  PART B - chat.deepseek.com: automatic submission                  */
  /* ------------------------------------------------------------------ */
  function partB() {
    const query = GM_getValue(STORAGE_KEY, '');
    GM_deleteValue(STORAGE_KEY);
    if (!query || !query.trim()) {
      log('No pending question');
      return;
    }
    log('Pending question found, waiting for the chat UI to load...');

    waitForChatUI(UI_TIMEOUT_MS).then((ready) => {
      if (!ready) return;
      if (!setInputText(query)) return;
      sleep(500)
        .then(enableWebSearch)
        .then(() => sleep(500))
        .then(sendMessage);
    });
  }

  function waitForChatUI(timeoutMs) {
    return new Promise((resolve) => {
      const started = Date.now();
      const check = () => {
        if (queryFirst(INPUT_SELECTORS)) {
          log('Chat UI ready');
          return resolve(true);
        }
        if (Date.now() - started > timeoutMs) {
          warn('Timeout: DeepSeek UI not ready within ' + timeoutMs + ' ms');
          return resolve(false);
        }
        setTimeout(check, 500);
      };
      check();
    });
  }

  function setInputText(text) {
    const found = queryFirst(INPUT_SELECTORS);
    if (!found) {
      warn('Message input not found, no submission');
      return false;
    }
    const el = found.el;
    el.focus();

    if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
      const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
      setter.call(el, text);
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
      log('Text set (' + text.length + ' chars) via:', found.selector);
    } else if (el.isContentEditable) {
      try {
        document.execCommand('selectAll', false, null);
        document.execCommand('insertText', false, text);
      } catch (err) {
        el.textContent = text;
        el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
      }
      log('Text set (' + text.length + ' chars) via:', found.selector);
    } else {
      warn('Unsupported input type:', el.tagName);
      return false;
    }
    return true;
  }

  function isToggleActive(el) {
    const attr = el.getAttribute('aria-pressed');
    if (attr !== null) return attr === 'true';
    return /(^|\s)(active|on)(\s|$)/i.test(el.className || '') || (el.getAttribute('data-state') === 'on');
  }

  async function enableWebSearch() {
    const toggle = queryFirst(WEBSEARCH_TOGGLE_SELECTORS);
    if (toggle) {
      if (isToggleActive(toggle.el)) {
        log('Web search already active');
        return;
      }
      toggle.el.click();
      await sleep(300);
      if (isToggleActive(toggle.el)) {
        log('Web search enabled (toggle):', toggle.selector);
        return;
      }
      warn('Toggle clicked but not active:', toggle.selector);
    }

    const dropdown = queryFirst(MODE_DROPDOWN_SELECTORS);
    if (dropdown) {
      dropdown.el.click();
      await sleep(400);
      const items = [];
      for (const sel of MODE_ITEM_SELECTORS) {
        try {
          items.push(...Array.from(document.querySelectorAll(sel)));
        } catch (err) {}
      }
      const target = items.find((i) => MODE_ITEM_TEXT.some((t) => (i.textContent || '').includes(t)));
      if (target) {
        target.click();
        log('Web search enabled (menu item):', target.textContent.trim());
        return;
      }
      warn('"Web search" item not found in the opened menu');
      document.body.click();
    }

    warn('Web search toggle/item not found: sending without web search');
  }

  function sendMessage() {
    const send = queryFirst(SEND_SELECTORS);
    if (send) {
      send.el.click();
      log('Message sent (button):', send.selector);
      return;
    }
    const input = queryFirst(INPUT_SELECTORS);
    if (!input) {
      warn('No send button and no input: cannot send');
      return;
    }
    input.el.focus();
    const opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
    input.el.dispatchEvent(new KeyboardEvent('keydown', opts));
    input.el.dispatchEvent(new KeyboardEvent('keypress', opts));
    input.el.dispatchEvent(new KeyboardEvent('keyup', opts));
    log('Message sent (Enter)');
  }

  /* ------------------------------------------------------------------ */
  /*  Startup                                                            */
  /* ------------------------------------------------------------------ */
  function main() {
    if (location.hostname === DEEPSEEK_HOST) {
      partB();
    } else {
      partA();
    }
  }

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