AI Prompt Manager (DeepSeek)

Easily manage (save, edit, insert) reusable prompts on DeepSeek Chat. Adds a floating button.

当前为 2025-05-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AI Prompt Manager (DeepSeek)
// @namespace    https://github.com/insign/userscripts
// @version      2025.02.18.1758
// @description  Easily manage (save, edit, insert) reusable prompts on DeepSeek Chat. Adds a floating button.
// @author       Hélio <[email protected]>
// @license      WTFPL
// @match        https://chat.deepseek.com/*
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// ==/UserScript==

(function() {
	'use strict'

	// --- Constantes ---
	const MANAGER_ID = 'ds-prompt-manager-v2' // ID para o container principal do gerenciador
	const BUTTON_ID = 'ds-prompt-button-v2'   // ID para o botão flutuante
	const STORAGE_KEY = 'ds_prompts_v2'      // Chave para armazenar os prompts no GM storage
	const CSS_THEME = '#4D6BFE'              // Cor tema para a interface

	// --- Estado ---
	let prompts = [] // Array para guardar os prompts carregados/salvos

	/**
	 * Inicializa o script: carrega prompts, cria a interface e adiciona listeners.
	 */
	async function initialize() {
		try {
			// Carrega os prompts salvos ou inicializa com array vazio
			const storedPrompts = await GM.getValue(STORAGE_KEY, '[]') // Padrão como string JSON
			try {
				prompts = JSON.parse(storedPrompts)
				// Garante que seja um array, mesmo que o storage esteja corrompido
				if (!Array.isArray(prompts)) {
					console.warn('AI Prompt Manager: Invalid data found in storage, resetting.')
					prompts = []
					await GM.setValue(STORAGE_KEY, JSON.stringify([]))
				}
			} catch (parseError) {
				console.error('AI Prompt Manager: Failed to parse stored prompts, resetting.', parseError)
				prompts = []
				await GM.setValue(STORAGE_KEY, JSON.stringify([])) // Reseta se não conseguir parsear
			}

			// Cria os elementos da interface
			createManagerButton()
			createPromptManager()

			// Configura os listeners de eventos
			setupEventListeners()

			// Preenche a lista de prompts na interface
			refreshPromptList()

			console.log('AI Prompt Manager initialized successfully.')

		} catch (error) {
			console.error('AI Prompt Manager: Initialization failed:', error)
		}
	}

	/**
	 * Cria o botão flutuante (📋) para abrir o gerenciador.
	 */
	function createManagerButton() {
		// Evita criar múltiplos botões
		if (document.getElementById(BUTTON_ID)) return

		// Cria o elemento do botão
		const btn = document.createElement('div')
		btn.id = BUTTON_ID
		btn.innerHTML = '📋' // Ícone de prancheta
		btn.title = 'Open Prompt Manager' // Tooltip

		// Aplica estilos ao botão
		Object.assign(btn.style, {
			position: 'fixed',
			bottom: '85px', // Posição vertical ajustada
			right: '20px',
			width: '45px',
			height: '45px',
			background: CSS_THEME,
			color: 'white',
			borderRadius: '50%',
			cursor: 'pointer',
			display: 'flex',
			alignItems: 'center',
			justifyContent: 'center',
			zIndex: '2147483646', // Z-index alto, mas abaixo do gerenciador
			fontSize: '24px',
			boxShadow: '0 3px 10px rgba(0,0,0,0.25)', // Sombra mais pronunciada
			transition: 'transform 0.2s ease-out, background-color 0.2s ease-out', // Transições suaves
			userSelect: 'none',
		})

		// Efeito hover
		btn.onmouseover = () => { btn.style.transform = 'scale(1.1)'; btn.style.backgroundColor = '#3b5ae0'; }
		btn.onmouseout = () => { btn.style.transform = 'scale(1)'; btn.style.backgroundColor = CSS_THEME; }

		document.body.appendChild(btn)
	}

	/**
	 * Cria o container do gerenciador de prompts (inicialmente oculto).
	 */
	function createPromptManager() {
		// Evita criar múltiplos gerenciadores
		if (document.getElementById(MANAGER_ID)) return

		// Cria o container principal
		const mgr = document.createElement('div')
		mgr.id = MANAGER_ID
		mgr.innerHTML = `
      <div class="ds-pm-header">
        <span>Saved Prompts</span>
        <button class="ds-pm-close-btn" title="Close Manager">×</button>
      </div>
      <div class="ds-pm-prompt-list"></div>
      <button class="ds-pm-add-prompt">+ New Prompt</button>
    `

		// Aplica estilos ao container
		Object.assign(mgr.style, {
			position: 'fixed',
			bottom: '140px', // Acima do botão flutuante
			right: '20px',
			background: 'white',
			borderRadius: '12px',
			boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
			padding: '0', // Padding será interno nos elementos filhos
			width: '320px', // Largura aumentada ligeiramente
			display: 'none', // Começa oculto
			zIndex: '2147483647', // Z-index máximo
			fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
			fontSize: '14px',
			overflow: 'hidden', // Para conter os elementos internos e bordas arredondadas
			border: '1px solid #e0e0e0',
		})

		document.body.appendChild(mgr)

		// Adiciona estilos CSS específicos via GM.addStyle
		addManagerStyles()
	}

	/**
	 * Atualiza a lista de prompts exibida na interface do gerenciador.
	 */
	function refreshPromptList() {
		const list = document.querySelector(`#${MANAGER_ID} .ds-pm-prompt-list`)
		if (!list) return // Sai se a lista não for encontrada

		list.innerHTML = '' // Limpa a lista atual

		if (prompts.length === 0) {
			list.innerHTML = '<div class="ds-pm-no-prompts">No prompts saved yet. Click "+ New Prompt" to add one.</div>'
			return
		}

		// Cria e adiciona um item para cada prompt
		prompts.forEach((prompt, index) => {
			const item = document.createElement('div')
			item.className = 'ds-pm-prompt-item'
			item.title = `Click to insert prompt:\n"${prompt.content.substring(0, 100)}${prompt.content.length > 100 ? '...' : ''}"` // Tooltip com preview
			item.innerHTML = `
              <span class="ds-pm-prompt-title">${prompt.title}</span>
              <div class="ds-pm-prompt-actions">
                  <button class="ds-pm-edit-btn" title="Edit Prompt">✏️</button>
                  <button class="ds-pm-delete-btn" title="Delete Prompt">🗑️</button>
              </div>
            `

			// Listener para deletar
			item.querySelector('.ds-pm-delete-btn').addEventListener('click', (e) => {
				e.stopPropagation() // Impede que o clique no botão acione o clique no item
				deletePrompt(index)
			})

			// Listener para editar
			item.querySelector('.ds-pm-edit-btn').addEventListener('click', (e) => {
				e.stopPropagation()
				editPrompt(index)
			})

			// Listener para inserir o prompt ao clicar no item
			item.addEventListener('click', () => insertPrompt(prompt.content))

			list.appendChild(item)
		})
	}

	/**
	 * Salva o array de prompts atual no armazenamento do GM.
	 */
	async function savePrompts() {
		try {
			await GM.setValue(STORAGE_KEY, JSON.stringify(prompts))
		} catch (error) {
			console.error('AI Prompt Manager: Failed to save prompts:', error)
			alert('Error: Could not save prompts.') // Informa o usuário
		}
	}

	/**
	 * Deleta um prompt do array e atualiza a interface e o armazenamento.
	 * @param {number} index - O índice do prompt a ser deletado.
	 */
	async function deletePrompt(index) {
		// Confirmação antes de deletar
		if (!confirm(`Are you sure you want to delete the prompt "${prompts[index]?.title}"?`)) {
			return
		}
		prompts.splice(index, 1) // Remove o prompt do array
		await savePrompts()      // Salva as alterações
		refreshPromptList()      // Atualiza a lista na interface
	}

	/**
	 * Permite ao usuário editar o título e o conteúdo de um prompt existente.
	 * @param {number} index - O índice do prompt a ser editado.
	 */
	async function editPrompt(index) {
		const promptData = prompts[index]
		if (!promptData) return // Sai se o índice for inválido

		// Pede novo título, mantendo o atual como padrão
		const newTitle = prompt('Edit prompt title:', promptData.title)
		if (newTitle === null) return // Sai se o usuário cancelar

		// Pede novo conteúdo, mantendo o atual como padrão
		const newContent = prompt('Edit prompt content:', promptData.content)
		if (newContent === null) return // Sai se o usuário cancelar

		// Atualiza o prompt no array
		prompts[index] = { title: newTitle.trim() || 'Untitled', content: newContent.trim() }
		await savePrompts()   // Salva as alterações
		refreshPromptList()   // Atualiza a interface
	}

	/**
	 * Insere o conteúdo de um prompt na caixa de texto do chat do DeepSeek.
	 * Tenta manipular o estado do React e o DOM para garantir a inserção correta.
	 * @param {string} content - O conteúdo do prompt a ser inserido.
	 */
	function insertPrompt(content) {
		// Seletores específicos do DeepSeek (podem precisar de atualização se o site mudar)
		const textarea = document.getElementById('chat-input') // O textarea real (pode estar oculto)
		const visibleEditor = document.querySelector('.ds-editor-input-wrapper .ds-md-editor-tiptap') // O editor visível (TipTap/ProseMirror)

		if (!textarea || !visibleEditor) {
			console.error('AI Prompt Manager: Could not find DeepSeek chat input elements.')
			alert('Error: Could not find the chat input field.')
			return
		}

		try {
			// --- Método 1: Simular input no editor visível (mais robusto para editores ricos) ---
			// Foca o editor
			visibleEditor.focus()

			// Cria um evento de input para simular digitação (pode ser necessário para o React detectar)
			// Adiciona o conteúdo + duas quebras de linha no início do valor atual
			const newValue = content + '\n\n' + (textarea.value || '')

			// Tenta usar document.execCommand (pode funcionar em alguns casos)
			// Move o cursor para o início antes de inserir
			const selection = window.getSelection()
			const range = document.createRange()
			range.selectNodeContents(visibleEditor)
			range.collapse(true) // Colapsa para o início
			selection.removeAllRanges()
			selection.addRange(range)
			// Insere o texto (pode não funcionar perfeitamente com React/TipTap)
			// document.execCommand('insertText', false, content + '\n\n') // Comentado - menos confiável

			// --- Método 2: Manipulação direta e disparo de evento (Fallback/Alternativa) ---
			// Define o valor no textarea oculto (React pode ouvir isso)
			textarea.value = newValue

			// Dispara eventos de input e change no textarea para notificar o React
			textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }))
			textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))

			// Atualiza o conteúdo do editor visível (força a sincronização visual)
			// Encontra o parágrafo inicial ou cria um se não existir
			let firstParagraph = visibleEditor.querySelector('p')
			if (!firstParagraph) {
				firstParagraph = document.createElement('p')
				visibleEditor.appendChild(firstParagraph)
			}
			// Define o conteúdo do primeiro parágrafo
			// Adiciona quebras de linha <br> para simular o parágrafo
			firstParagraph.innerHTML = content.replace(/\n/g, '<br>') + '<br><br>' + firstParagraph.innerHTML


			// --- Método 3: Interagir com a instância do editor TipTap (Avançado, se possível) ---
			// Se houvesse uma forma de acessar a API do TipTap (ex: window.editorInstance),
			// seria o método ideal:
			// if (window.editorInstance) {
			//    window.editorInstance.chain().focus().insertContentAt(0, content + '\n\n').run()
			// }

			console.log('AI Prompt Manager: Prompt inserted.')
			// Fecha o gerenciador após a inserção
			hideManager()

		} catch (error) {
			console.error('AI Prompt Manager: Failed to insert prompt:', error)
			alert('Error: Could not insert the prompt into the chat input.')
		}
	}

	/**
	 * Esconde o painel do gerenciador.
	 */
	function hideManager() {
		const mgr = document.getElementById(MANAGER_ID)
		if (mgr) mgr.style.display = 'none'
	}

	/**
	 * Configura os listeners de eventos para o botão e o gerenciador.
	 */
	function setupEventListeners() {
		// Listener para o botão flutuante: mostra/esconde o gerenciador
		document.getElementById(BUTTON_ID)?.addEventListener('click', (e) => {
			e.stopPropagation() // Impede que o clique feche o gerenciador imediatamente
			const mgr = document.getElementById(MANAGER_ID)
			if (mgr) {
				mgr.style.display = mgr.style.display === 'none' ? 'block' : 'none'
			}
		})

		// Listener para fechar o gerenciador clicando fora dele ou no botão 'x'
		document.addEventListener('click', (e) => {
			const mgr = document.getElementById(MANAGER_ID)
			const btn = document.getElementById(BUTTON_ID)
			// Fecha se o clique foi fora do gerenciador E fora do botão de abrir
			// Ou se foi no botão de fechar dentro do header
			if (mgr && mgr.style.display === 'block') {
				if (e.target.classList.contains('ds-pm-close-btn')) {
					hideManager()
				} else if (!mgr.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
					hideManager()
				}
			}
		}, true) // Usa captura para pegar o evento antes que outros listeners o parem

		// Listener para o botão "+ New Prompt"
		document.querySelector(`#${MANAGER_ID} .ds-pm-add-prompt`)?.addEventListener('click', async (e) => {
			e.stopPropagation() // Previne fechar o painel
			const title = prompt('Enter prompt title:')
			if (title === null) return // Cancelado
			const content = prompt('Enter prompt content:')
			if (content === null) return // Cancelado

			// Adiciona o novo prompt ao array
			prompts.push({ title: title.trim() || 'Untitled', content: content.trim() })
			await savePrompts() // Salva
			refreshPromptList() // Atualiza a interface
		})
	}

	/**
	 * Adiciona os estilos CSS para o gerenciador usando GM.addStyle.
	 */
	function addManagerStyles() {
		GM.addStyle(`
          #${MANAGER_ID} * { /* Reseta box-sizing para consistência */
             box-sizing: border-box;
          }
          #${MANAGER_ID} .ds-pm-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 15px;
            background: #f7f7f7;
            border-bottom: 1px solid #e0e0e0;
            font-weight: 600;
            color: #333;
            font-size: 15px;
          }
          #${MANAGER_ID} .ds-pm-close-btn {
            background: none;
            border: none;
            font-size: 20px;
            cursor: pointer;
            color: #888;
            padding: 0 5px;
            line-height: 1;
          }
           #${MANAGER_ID} .ds-pm-close-btn:hover {
            color: #000;
           }
          #${MANAGER_ID} .ds-pm-prompt-list {
            max-height: 40vh; /* Limita altura da lista */
            overflow-y: auto; /* Adiciona scroll se necessário */
            padding: 8px;
          }
          #${MANAGER_ID} .ds-pm-no-prompts {
             text-align: center;
             color: #777;
             padding: 20px;
             font-style: italic;
          }
          #${MANAGER_ID} .ds-pm-prompt-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 12px;
            margin-bottom: 6px;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.15s ease-out;
            border: 1px solid transparent; /* Para manter o layout no hover */
          }
          #${MANAGER_ID} .ds-pm-prompt-item:hover {
            background-color: #f0f4ff; /* Cor de fundo suave no hover */
            border-color: #d0dfff;
          }
          #${MANAGER_ID} .ds-pm-prompt-title {
            flex-grow: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis; /* Adiciona '...' se o título for longo */
            margin-right: 10px;
            color: #222;
          }
          #${MANAGER_ID} .ds-pm-prompt-actions button {
            background: none;
            border: none;
            padding: 2px 4px; /* Padding ajustado */
            cursor: pointer;
            margin-left: 5px; /* Espaço entre botões */
            opacity: 0.6;
            transition: opacity 0.15s ease-out;
            font-size: 14px; /* Tamanho dos ícones (emojis) */
          }
          #${MANAGER_ID} .ds-pm-prompt-actions button:hover {
            opacity: 1;
          }
          #${MANAGER_ID} .ds-pm-add-prompt {
            display: block; /* Ocupa toda a largura */
            width: calc(100% - 20px); /* Largura ajustada para padding */
            margin: 10px; /* Margem em volta */
            padding: 10px;
            background: ${CSS_THEME};
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 500;
            text-align: center;
            font-size: 14px;
            transition: background-color 0.15s ease-out;
          }
          #${MANAGER_ID} .ds-pm-add-prompt:hover {
            background-color: #3b5ae0; /* Cor mais escura no hover */
          }

          /* Estilo da barra de scroll */
           #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar {
              width: 6px;
            }
            #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-track {
              background: #f1f1f1;
              border-radius: 3px;
            }
            #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-thumb {
              background: #ccc;
              border-radius: 3px;
            }
            #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-thumb:hover {
              background: #aaa;
            }
        `)
	}


	// --- Inicialização ---
	// Espera um pouco para garantir que o DOM do DeepSeek esteja mais estável
	// antes de tentar adicionar elementos e listeners.
	if (document.readyState === 'complete') {
		setTimeout(initialize, 1000)
	} else {
		window.addEventListener('load', () => setTimeout(initialize, 1000))
	}

})();