DeepSeek Input Restore

Automatically saves your message draft to localStorage and restores it when you refresh the page. Never lose your input again.

이 스크립트를 설치하려면 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         DeepSeek Input Restore
// @namespace    https://kyoya.is-a.dev
// @version      1.2.0
// @description  Automatically saves your message draft to localStorage and restores it when you refresh the page. Never lose your input again.
// @description:tr Mesaj taslağınızı otomatik olarak localStorage'a kaydeder ve sayfayı yenilediğinizde geri yükler. Bir daha yazdıklarınızı kaybetmeyin.
// @description:zh-CN 自动将您的消息草稿保存到 localStorage,刷新页面时自动恢复。再也不会丢失输入内容。
// @author       kyoyacchi
// @license      MIT
// @icon         https://deepseek.com/favicon.ico
// @match        https://chat.deepseek.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = 'ds_input_draft';

  // ─── Robust textarea finder ─────────────────────────────────────────────────
  // The name="search" attribute is language-independent and stays fixed in
  // DeepSeek's internal markup. We never rely on the placeholder as primary.

  function findTextarea() {
    // 1. Priority: name attribute (language-independent, most reliable)
    const byName = document.querySelector('textarea[name="search"]');
    if (byName) return byName;

    // 2. Fallback: the single visible textarea on the page
    const candidates = Array.from(document.querySelectorAll('textarea'))
      .filter(el => el.offsetParent !== null);

    if (candidates.length === 1) return candidates[0];

    // 3. Last resort: placeholder (try both EN/TR)
    const byPlaceholder = candidates.find(el => {
      const ph = (el.getAttribute('placeholder') || '').toLowerCase();
      return ph.includes('deepseek') || ph.includes('message') || ph.includes('mesaj');
    });
    if (byPlaceholder) return byPlaceholder;

    return candidates[candidates.length - 1] || null;
  }

  function findSendButton() {
    // Design system class, language-independent
    return document.querySelector('div[role="button"].ds-button--primary.ds-button--circle');
  }

  // ─── Helpers ────────────────────────────────────────────────────────────────

  function save(value) {
    if (value) {
      localStorage.setItem(STORAGE_KEY, value);
    } else {
      localStorage.removeItem(STORAGE_KEY);
    }
  }

  function restore(textarea) {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (!saved || !textarea) return;

    const nativeSetter = Object.getOwnPropertyDescriptor(
      HTMLTextAreaElement.prototype,
      'value'
    ).set;

    nativeSetter.call(textarea, saved);
    textarea.dispatchEvent(new Event('input',  { bubbles: true }));
    textarea.dispatchEvent(new Event('change', { bubbles: true }));
    textarea.focus();
  }

  // ─── Listener setup ─────────────────────────────────────────────────────────

  function attachListeners(textarea) {
    // Save on every keystroke
    textarea.addEventListener('input', () => save(textarea.value));

    // Clear draft when Enter is pressed (message sent)
    textarea.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        localStorage.removeItem(STORAGE_KEY);
      }
    });
  }

  // Clear draft when the send button is clicked
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('div[role="button"].ds-button--primary.ds-button--circle');
    if (btn) localStorage.removeItem(STORAGE_KEY);
  }, true);

  // ─── Wait for textarea ───────────────────────────────────────────────────────

  // DeepSeek is a SPA so the textarea might not exist on document-idle.
  // We observe the DOM until it appears, then attach listeners and restore.
  // We also re-check on every new chat navigation (textarea can be re-mounted).
  let knownTextarea = null;

  function init() {
    const textarea = findTextarea();

    if (textarea && textarea !== knownTextarea) {
      knownTextarea = textarea;
      attachListeners(textarea);

      // Small delay so React finishes hydrating before we write the value
      setTimeout(() => restore(textarea), 400);
    }
  }

  const observer = new MutationObserver(init);
  observer.observe(document.body, { childList: true, subtree: true });

  // Also try immediately in case it's already rendered
  init();
})();