AI Prompt Manager (DeepSeek)

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

  1. // ==UserScript==
  2. // @name AI Prompt Manager (DeepSeek)
  3. // @namespace https://github.com/insign/userscripts
  4. // @version 2025.02.18.1758
  5. // @description Easily manage (save, edit, insert) reusable prompts on DeepSeek Chat. Adds a floating button.
  6. // @author Hélio <open@helio.me>
  7. // @license WTFPL
  8. // @match https://chat.deepseek.com/*
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // @grant GM.addStyle
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict'
  16.  
  17. // --- Constantes ---
  18. const MANAGER_ID = 'ds-prompt-manager-v2' // ID para o container principal do gerenciador
  19. const BUTTON_ID = 'ds-prompt-button-v2' // ID para o botão flutuante
  20. const STORAGE_KEY = 'ds_prompts_v2' // Chave para armazenar os prompts no GM storage
  21. const CSS_THEME = '#4D6BFE' // Cor tema para a interface
  22.  
  23. // --- Estado ---
  24. let prompts = [] // Array para guardar os prompts carregados/salvos
  25.  
  26. /**
  27. * Inicializa o script: carrega prompts, cria a interface e adiciona listeners.
  28. */
  29. async function initialize() {
  30. try {
  31. // Carrega os prompts salvos ou inicializa com array vazio
  32. const storedPrompts = await GM.getValue(STORAGE_KEY, '[]') // Padrão como string JSON
  33. try {
  34. prompts = JSON.parse(storedPrompts)
  35. // Garante que seja um array, mesmo que o storage esteja corrompido
  36. if (!Array.isArray(prompts)) {
  37. console.warn('AI Prompt Manager: Invalid data found in storage, resetting.')
  38. prompts = []
  39. await GM.setValue(STORAGE_KEY, JSON.stringify([]))
  40. }
  41. } catch (parseError) {
  42. console.error('AI Prompt Manager: Failed to parse stored prompts, resetting.', parseError)
  43. prompts = []
  44. await GM.setValue(STORAGE_KEY, JSON.stringify([])) // Reseta se não conseguir parsear
  45. }
  46.  
  47. // Cria os elementos da interface
  48. createManagerButton()
  49. createPromptManager()
  50.  
  51. // Configura os listeners de eventos
  52. setupEventListeners()
  53.  
  54. // Preenche a lista de prompts na interface
  55. refreshPromptList()
  56.  
  57. console.log('AI Prompt Manager initialized successfully.')
  58.  
  59. } catch (error) {
  60. console.error('AI Prompt Manager: Initialization failed:', error)
  61. }
  62. }
  63.  
  64. /**
  65. * Cria o botão flutuante (📋) para abrir o gerenciador.
  66. */
  67. function createManagerButton() {
  68. // Evita criar múltiplos botões
  69. if (document.getElementById(BUTTON_ID)) return
  70.  
  71. // Cria o elemento do botão
  72. const btn = document.createElement('div')
  73. btn.id = BUTTON_ID
  74. btn.innerHTML = '📋' // Ícone de prancheta
  75. btn.title = 'Open Prompt Manager' // Tooltip
  76.  
  77. // Aplica estilos ao botão
  78. Object.assign(btn.style, {
  79. position: 'fixed',
  80. bottom: '85px', // Posição vertical ajustada
  81. right: '20px',
  82. width: '45px',
  83. height: '45px',
  84. background: CSS_THEME,
  85. color: 'white',
  86. borderRadius: '50%',
  87. cursor: 'pointer',
  88. display: 'flex',
  89. alignItems: 'center',
  90. justifyContent: 'center',
  91. zIndex: '2147483646', // Z-index alto, mas abaixo do gerenciador
  92. fontSize: '24px',
  93. boxShadow: '0 3px 10px rgba(0,0,0,0.25)', // Sombra mais pronunciada
  94. transition: 'transform 0.2s ease-out, background-color 0.2s ease-out', // Transições suaves
  95. userSelect: 'none',
  96. })
  97.  
  98. // Efeito hover
  99. btn.onmouseover = () => { btn.style.transform = 'scale(1.1)'; btn.style.backgroundColor = '#3b5ae0'; }
  100. btn.onmouseout = () => { btn.style.transform = 'scale(1)'; btn.style.backgroundColor = CSS_THEME; }
  101.  
  102. document.body.appendChild(btn)
  103. }
  104.  
  105. /**
  106. * Cria o container do gerenciador de prompts (inicialmente oculto).
  107. */
  108. function createPromptManager() {
  109. // Evita criar múltiplos gerenciadores
  110. if (document.getElementById(MANAGER_ID)) return
  111.  
  112. // Cria o container principal
  113. const mgr = document.createElement('div')
  114. mgr.id = MANAGER_ID
  115. mgr.innerHTML = `
  116. <div class="ds-pm-header">
  117. <span>Saved Prompts</span>
  118. <button class="ds-pm-close-btn" title="Close Manager">×</button>
  119. </div>
  120. <div class="ds-pm-prompt-list"></div>
  121. <button class="ds-pm-add-prompt">+ New Prompt</button>
  122. `
  123.  
  124. // Aplica estilos ao container
  125. Object.assign(mgr.style, {
  126. position: 'fixed',
  127. bottom: '140px', // Acima do botão flutuante
  128. right: '20px',
  129. background: 'white',
  130. borderRadius: '12px',
  131. boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
  132. padding: '0', // Padding será interno nos elementos filhos
  133. width: '320px', // Largura aumentada ligeiramente
  134. display: 'none', // Começa oculto
  135. zIndex: '2147483647', // Z-index máximo
  136. fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
  137. fontSize: '14px',
  138. overflow: 'hidden', // Para conter os elementos internos e bordas arredondadas
  139. border: '1px solid #e0e0e0',
  140. })
  141.  
  142. document.body.appendChild(mgr)
  143.  
  144. // Adiciona estilos CSS específicos via GM.addStyle
  145. addManagerStyles()
  146. }
  147.  
  148. /**
  149. * Atualiza a lista de prompts exibida na interface do gerenciador.
  150. */
  151. function refreshPromptList() {
  152. const list = document.querySelector(`#${MANAGER_ID} .ds-pm-prompt-list`)
  153. if (!list) return // Sai se a lista não for encontrada
  154.  
  155. list.innerHTML = '' // Limpa a lista atual
  156.  
  157. if (prompts.length === 0) {
  158. list.innerHTML = '<div class="ds-pm-no-prompts">No prompts saved yet. Click "+ New Prompt" to add one.</div>'
  159. return
  160. }
  161.  
  162. // Cria e adiciona um item para cada prompt
  163. prompts.forEach((prompt, index) => {
  164. const item = document.createElement('div')
  165. item.className = 'ds-pm-prompt-item'
  166. item.title = `Click to insert prompt:\n"${prompt.content.substring(0, 100)}${prompt.content.length > 100 ? '...' : ''}"` // Tooltip com preview
  167. item.innerHTML = `
  168. <span class="ds-pm-prompt-title">${prompt.title}</span>
  169. <div class="ds-pm-prompt-actions">
  170. <button class="ds-pm-edit-btn" title="Edit Prompt">✏️</button>
  171. <button class="ds-pm-delete-btn" title="Delete Prompt">🗑️</button>
  172. </div>
  173. `
  174.  
  175. // Listener para deletar
  176. item.querySelector('.ds-pm-delete-btn').addEventListener('click', (e) => {
  177. e.stopPropagation() // Impede que o clique no botão acione o clique no item
  178. deletePrompt(index)
  179. })
  180.  
  181. // Listener para editar
  182. item.querySelector('.ds-pm-edit-btn').addEventListener('click', (e) => {
  183. e.stopPropagation()
  184. editPrompt(index)
  185. })
  186.  
  187. // Listener para inserir o prompt ao clicar no item
  188. item.addEventListener('click', () => insertPrompt(prompt.content))
  189.  
  190. list.appendChild(item)
  191. })
  192. }
  193.  
  194. /**
  195. * Salva o array de prompts atual no armazenamento do GM.
  196. */
  197. async function savePrompts() {
  198. try {
  199. await GM.setValue(STORAGE_KEY, JSON.stringify(prompts))
  200. } catch (error) {
  201. console.error('AI Prompt Manager: Failed to save prompts:', error)
  202. alert('Error: Could not save prompts.') // Informa o usuário
  203. }
  204. }
  205.  
  206. /**
  207. * Deleta um prompt do array e atualiza a interface e o armazenamento.
  208. * @param {number} index - O índice do prompt a ser deletado.
  209. */
  210. async function deletePrompt(index) {
  211. // Confirmação antes de deletar
  212. if (!confirm(`Are you sure you want to delete the prompt "${prompts[index]?.title}"?`)) {
  213. return
  214. }
  215. prompts.splice(index, 1) // Remove o prompt do array
  216. await savePrompts() // Salva as alterações
  217. refreshPromptList() // Atualiza a lista na interface
  218. }
  219.  
  220. /**
  221. * Permite ao usuário editar o título e o conteúdo de um prompt existente.
  222. * @param {number} index - O índice do prompt a ser editado.
  223. */
  224. async function editPrompt(index) {
  225. const promptData = prompts[index]
  226. if (!promptData) return // Sai se o índice for inválido
  227.  
  228. // Pede novo título, mantendo o atual como padrão
  229. const newTitle = prompt('Edit prompt title:', promptData.title)
  230. if (newTitle === null) return // Sai se o usuário cancelar
  231.  
  232. // Pede novo conteúdo, mantendo o atual como padrão
  233. const newContent = prompt('Edit prompt content:', promptData.content)
  234. if (newContent === null) return // Sai se o usuário cancelar
  235.  
  236. // Atualiza o prompt no array
  237. prompts[index] = { title: newTitle.trim() || 'Untitled', content: newContent.trim() }
  238. await savePrompts() // Salva as alterações
  239. refreshPromptList() // Atualiza a interface
  240. }
  241.  
  242. /**
  243. * Insere o conteúdo de um prompt na caixa de texto do chat do DeepSeek.
  244. * Tenta manipular o estado do React e o DOM para garantir a inserção correta.
  245. * @param {string} content - O conteúdo do prompt a ser inserido.
  246. */
  247. function insertPrompt(content) {
  248. // Seletores específicos do DeepSeek (podem precisar de atualização se o site mudar)
  249. const textarea = document.getElementById('chat-input') // O textarea real (pode estar oculto)
  250. const visibleEditor = document.querySelector('.ds-editor-input-wrapper .ds-md-editor-tiptap') // O editor visível (TipTap/ProseMirror)
  251.  
  252. if (!textarea || !visibleEditor) {
  253. console.error('AI Prompt Manager: Could not find DeepSeek chat input elements.')
  254. alert('Error: Could not find the chat input field.')
  255. return
  256. }
  257.  
  258. try {
  259. // --- Método 1: Simular input no editor visível (mais robusto para editores ricos) ---
  260. // Foca o editor
  261. visibleEditor.focus()
  262.  
  263. // Cria um evento de input para simular digitação (pode ser necessário para o React detectar)
  264. // Adiciona o conteúdo + duas quebras de linha no início do valor atual
  265. const newValue = content + '\n\n' + (textarea.value || '')
  266.  
  267. // Tenta usar document.execCommand (pode funcionar em alguns casos)
  268. // Move o cursor para o início antes de inserir
  269. const selection = window.getSelection()
  270. const range = document.createRange()
  271. range.selectNodeContents(visibleEditor)
  272. range.collapse(true) // Colapsa para o início
  273. selection.removeAllRanges()
  274. selection.addRange(range)
  275. // Insere o texto (pode não funcionar perfeitamente com React/TipTap)
  276. // document.execCommand('insertText', false, content + '\n\n') // Comentado - menos confiável
  277.  
  278. // --- Método 2: Manipulação direta e disparo de evento (Fallback/Alternativa) ---
  279. // Define o valor no textarea oculto (React pode ouvir isso)
  280. textarea.value = newValue
  281.  
  282. // Dispara eventos de input e change no textarea para notificar o React
  283. textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }))
  284. textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }))
  285.  
  286. // Atualiza o conteúdo do editor visível (força a sincronização visual)
  287. // Encontra o parágrafo inicial ou cria um se não existir
  288. let firstParagraph = visibleEditor.querySelector('p')
  289. if (!firstParagraph) {
  290. firstParagraph = document.createElement('p')
  291. visibleEditor.appendChild(firstParagraph)
  292. }
  293. // Define o conteúdo do primeiro parágrafo
  294. // Adiciona quebras de linha <br> para simular o parágrafo
  295. firstParagraph.innerHTML = content.replace(/\n/g, '<br>') + '<br><br>' + firstParagraph.innerHTML
  296.  
  297.  
  298. // --- Método 3: Interagir com a instância do editor TipTap (Avançado, se possível) ---
  299. // Se houvesse uma forma de acessar a API do TipTap (ex: window.editorInstance),
  300. // seria o método ideal:
  301. // if (window.editorInstance) {
  302. // window.editorInstance.chain().focus().insertContentAt(0, content + '\n\n').run()
  303. // }
  304.  
  305. console.log('AI Prompt Manager: Prompt inserted.')
  306. // Fecha o gerenciador após a inserção
  307. hideManager()
  308.  
  309. } catch (error) {
  310. console.error('AI Prompt Manager: Failed to insert prompt:', error)
  311. alert('Error: Could not insert the prompt into the chat input.')
  312. }
  313. }
  314.  
  315. /**
  316. * Esconde o painel do gerenciador.
  317. */
  318. function hideManager() {
  319. const mgr = document.getElementById(MANAGER_ID)
  320. if (mgr) mgr.style.display = 'none'
  321. }
  322.  
  323. /**
  324. * Configura os listeners de eventos para o botão e o gerenciador.
  325. */
  326. function setupEventListeners() {
  327. // Listener para o botão flutuante: mostra/esconde o gerenciador
  328. document.getElementById(BUTTON_ID)?.addEventListener('click', (e) => {
  329. e.stopPropagation() // Impede que o clique feche o gerenciador imediatamente
  330. const mgr = document.getElementById(MANAGER_ID)
  331. if (mgr) {
  332. mgr.style.display = mgr.style.display === 'none' ? 'block' : 'none'
  333. }
  334. })
  335.  
  336. // Listener para fechar o gerenciador clicando fora dele ou no botão 'x'
  337. document.addEventListener('click', (e) => {
  338. const mgr = document.getElementById(MANAGER_ID)
  339. const btn = document.getElementById(BUTTON_ID)
  340. // Fecha se o clique foi fora do gerenciador E fora do botão de abrir
  341. // Ou se foi no botão de fechar dentro do header
  342. if (mgr && mgr.style.display === 'block') {
  343. if (e.target.classList.contains('ds-pm-close-btn')) {
  344. hideManager()
  345. } else if (!mgr.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
  346. hideManager()
  347. }
  348. }
  349. }, true) // Usa captura para pegar o evento antes que outros listeners o parem
  350.  
  351. // Listener para o botão "+ New Prompt"
  352. document.querySelector(`#${MANAGER_ID} .ds-pm-add-prompt`)?.addEventListener('click', async (e) => {
  353. e.stopPropagation() // Previne fechar o painel
  354. const title = prompt('Enter prompt title:')
  355. if (title === null) return // Cancelado
  356. const content = prompt('Enter prompt content:')
  357. if (content === null) return // Cancelado
  358.  
  359. // Adiciona o novo prompt ao array
  360. prompts.push({ title: title.trim() || 'Untitled', content: content.trim() })
  361. await savePrompts() // Salva
  362. refreshPromptList() // Atualiza a interface
  363. })
  364. }
  365.  
  366. /**
  367. * Adiciona os estilos CSS para o gerenciador usando GM.addStyle.
  368. */
  369. function addManagerStyles() {
  370. GM.addStyle(`
  371. #${MANAGER_ID} * { /* Reseta box-sizing para consistência */
  372. box-sizing: border-box;
  373. }
  374. #${MANAGER_ID} .ds-pm-header {
  375. display: flex;
  376. justify-content: space-between;
  377. align-items: center;
  378. padding: 10px 15px;
  379. background: #f7f7f7;
  380. border-bottom: 1px solid #e0e0e0;
  381. font-weight: 600;
  382. color: #333;
  383. font-size: 15px;
  384. }
  385. #${MANAGER_ID} .ds-pm-close-btn {
  386. background: none;
  387. border: none;
  388. font-size: 20px;
  389. cursor: pointer;
  390. color: #888;
  391. padding: 0 5px;
  392. line-height: 1;
  393. }
  394. #${MANAGER_ID} .ds-pm-close-btn:hover {
  395. color: #000;
  396. }
  397. #${MANAGER_ID} .ds-pm-prompt-list {
  398. max-height: 40vh; /* Limita altura da lista */
  399. overflow-y: auto; /* Adiciona scroll se necessário */
  400. padding: 8px;
  401. }
  402. #${MANAGER_ID} .ds-pm-no-prompts {
  403. text-align: center;
  404. color: #777;
  405. padding: 20px;
  406. font-style: italic;
  407. }
  408. #${MANAGER_ID} .ds-pm-prompt-item {
  409. display: flex;
  410. justify-content: space-between;
  411. align-items: center;
  412. padding: 10px 12px;
  413. margin-bottom: 6px;
  414. border-radius: 6px;
  415. cursor: pointer;
  416. transition: background-color 0.15s ease-out;
  417. border: 1px solid transparent; /* Para manter o layout no hover */
  418. }
  419. #${MANAGER_ID} .ds-pm-prompt-item:hover {
  420. background-color: #f0f4ff; /* Cor de fundo suave no hover */
  421. border-color: #d0dfff;
  422. }
  423. #${MANAGER_ID} .ds-pm-prompt-title {
  424. flex-grow: 1;
  425. white-space: nowrap;
  426. overflow: hidden;
  427. text-overflow: ellipsis; /* Adiciona '...' se o título for longo */
  428. margin-right: 10px;
  429. color: #222;
  430. }
  431. #${MANAGER_ID} .ds-pm-prompt-actions button {
  432. background: none;
  433. border: none;
  434. padding: 2px 4px; /* Padding ajustado */
  435. cursor: pointer;
  436. margin-left: 5px; /* Espaço entre botões */
  437. opacity: 0.6;
  438. transition: opacity 0.15s ease-out;
  439. font-size: 14px; /* Tamanho dos ícones (emojis) */
  440. }
  441. #${MANAGER_ID} .ds-pm-prompt-actions button:hover {
  442. opacity: 1;
  443. }
  444. #${MANAGER_ID} .ds-pm-add-prompt {
  445. display: block; /* Ocupa toda a largura */
  446. width: calc(100% - 20px); /* Largura ajustada para padding */
  447. margin: 10px; /* Margem em volta */
  448. padding: 10px;
  449. background: ${CSS_THEME};
  450. color: white;
  451. border: none;
  452. border-radius: 6px;
  453. cursor: pointer;
  454. font-weight: 500;
  455. text-align: center;
  456. font-size: 14px;
  457. transition: background-color 0.15s ease-out;
  458. }
  459. #${MANAGER_ID} .ds-pm-add-prompt:hover {
  460. background-color: #3b5ae0; /* Cor mais escura no hover */
  461. }
  462.  
  463. /* Estilo da barra de scroll */
  464. #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar {
  465. width: 6px;
  466. }
  467. #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-track {
  468. background: #f1f1f1;
  469. border-radius: 3px;
  470. }
  471. #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-thumb {
  472. background: #ccc;
  473. border-radius: 3px;
  474. }
  475. #${MANAGER_ID} .ds-pm-prompt-list::-webkit-scrollbar-thumb:hover {
  476. background: #aaa;
  477. }
  478. `)
  479. }
  480.  
  481.  
  482. // --- Inicialização ---
  483. // Espera um pouco para garantir que o DOM do DeepSeek esteja mais estável
  484. // antes de tentar adicionar elementos e listeners.
  485. if (document.readyState === 'complete') {
  486. setTimeout(initialize, 1000)
  487. } else {
  488. window.addEventListener('load', () => setTimeout(initialize, 1000))
  489. }
  490.  
  491. })();