您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily manage (save, edit, insert) reusable prompts on DeepSeek Chat. Adds a floating button.
// ==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)) } })();