// ==UserScript==
// @name My Prompt
// @name:en My Prompt
// @name:pt-BR Meu Prompt
// @name:es Mi Prompt
// @name:zh-CN 我的提示
// @namespace https://github.com/0H4S
// @version 1.3
// @description Save, edit, delete, import, and export your custom prompts directly in ChatGPT, DeepSeek, Google AI Studio, Qwen, Z.ai, Gemini, and LMArena with a modern UI.
// @description:en Save, edit, delete, import, and export your custom prompts directly in ChatGPT, DeepSeek, Google AI Studio, Qwen, Z.ai, Gemini, and LMArena with a modern UI.
// @description:pt-BR Salve, edite, exclua, importe e exporte seus prompts personalizados diretamente no ChatGPT, DeepSeek, Google AI Studio, Qwen, Z.ai, Gemini e LMArena com uma UI moderna.
// @description:es Guarda, edita, elimina, importa y exporta tus prompts personalizados directamente en ChatGPT, DeepSeek, Google AI Studio, Qwen, Z.ai, Gemini y LMArena con una UI moderna.
// @description:zh-CN 直接在 ChatGPT、DeepSeek、Google AI Studio、Qwen、Z.ai、Gemini 和 LMArena 中保存、编辑、删除、导入和导出您的自定义提示,拥有现代化的用户界面。
// @author OHAS
// @homepage https://github.com/0H4S
// @icon https://cdn-icons-png.flaticon.com/512/4997/4997543.png
// @license CC-BY-NC-ND-4.0
// @copyright 2025 OHAS. All Rights Reserved.
// @match https://chatgpt.com/*
// @match https://chat.deepseek.com/*
// @match https://aistudio.google.com/*
// @match https://chat.qwen.ai/*
// @match https://chat.z.ai/*
// @match https://gemini.google.com/*
// @match https://lmarena.ai/*
// @require https://update.greasyfork.org/scripts/549920/Script%20Notifier.js
// @connect gist.githubusercontent.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
if (window.top !== window.self) return;
const translations = {
'en': {
langName: 'English',
prompt: 'Prompt',
prompts: 'Prompts',
newPrompt: 'New Prompt',
editPrompt: 'Edit Prompt',
title: 'Title',
text: 'Prompt',
save: 'Save',
close: 'Close',
edit: 'Edit',
delete: 'Delete',
noSavedPrompts: 'No saved prompts.',
addPrompt: 'Add prompt',
import: 'Import',
export: 'Export',
confirmDelete: 'Delete prompt "{title}"?',
noPromptsToExport: 'No prompts to export.',
promptsImported: '{count} prompts imported successfully!',
errorImporting: 'Error importing file: {error}',
requiredFields: 'Title and prompt are required.',
editorNotFound: 'Could not find the text area for {platform}.',
languageSettings: '🌐 Language',
fileName: 'My Prompts.json'
},
'es': {
langName: 'Español',
prompt: 'Prompt',
prompts: 'Prompts',
newPrompt: 'Nuevo Prompt',
editPrompt: 'Editar Prompt',
title: 'Título',
text: 'Prompt',
save: 'Guardar',
close: 'Cerrar',
edit: 'Editar',
delete: 'Eliminar',
noSavedPrompts: 'No hay prompts guardados.',
addPrompt: 'Añadir prompt',
import: 'Importar',
export: 'Exportar',
confirmDelete: '¿Eliminar prompt "{title}"?',
noPromptsToExport: 'No hay prompts para exportar.',
promptsImported: '¡{count} prompts importados con éxito!',
errorImporting: 'Error al importar el archivo: {error}',
requiredFields: 'El título y el prompt son obligatorios.',
editorNotFound: 'No se pudo encontrar el área de texto para {platform}.',
languageSettings: '🌐 Idioma',
fileName: 'Mis Prompts.json'
},
'pt-BR': {
langName: 'Português (BR)',
prompt: 'Prompt',
prompts: 'Prompts',
newPrompt: 'Novo Prompt',
editPrompt: 'Editar Prompt',
title: 'Título',
text: 'Prompt',
save: 'Salvar',
close: 'Fechar',
edit: 'Editar',
delete: 'Excluir',
noSavedPrompts: 'Nenhum prompt salvo.',
addPrompt: 'Adicionar prompt',
import: 'Importar',
export: 'Exportar',
confirmDelete: 'Excluir prompt "{title}"?',
noPromptsToExport: 'Não há prompts para exportar.',
promptsImported: '{count} prompts importados com sucesso!',
errorImporting: 'Erro ao importar o arquivo: {error}',
requiredFields: 'Título e prompt são obrigatórios.',
editorNotFound: 'Não foi possível encontrar a área de texto para {platform}.',
languageSettings: '🌐 Idioma',
fileName: 'Meus Prompts.json'
},
'zh-CN': {
langName: '简体中文',
prompt: '提示',
prompts: '提示',
newPrompt: '新建提示',
editPrompt: '编辑提示',
title: '标题',
text: '提示内容',
save: '保存',
close: '关闭',
edit: '编辑',
delete: '删除',
noSavedPrompts: '没有已保存的提示。',
addPrompt: '添加提示',
import: '导入',
export: '导出',
confirmDelete: '确定要删除提示 "{title}" 吗?',
noPromptsToExport: '沒有可導出的提示。',
promptsImported: '成功导入 {count} 个提示!',
errorImporting: '导入文件时出错: {error}',
requiredFields: '标题和提示内容为必填项。',
editorNotFound: '未能找到 {platform} 的文本输入区域。',
languageSettings: '🌐 语言',
fileName: '我的提示.json'
}
};
const LANG_STORAGE_KEY = 'UserScriptLang';
let currentLang = 'en';
const SCRIPT_CONFIG = {
notificationsUrl: 'https://gist.githubusercontent.com/0H4S/40b2a2feb2ba18d0bf63a1943ba5cec3/raw/my_prompt_notifications.json',
scriptVersion: '1.3',
};
const notifier = new ScriptNotifier(SCRIPT_CONFIG);
notifier.run();
const PROMPT_STORAGE_KEY = 'Prompts';
let isInitialized = false;
let isInitializing = false;
let currentButton = null;
let currentPlatform = null;
let pageObserver = null;
let currentMenu = null;
let currentModal = null;
let languageModal = null;
const scriptPolicy = window.trustedTypes
? window.trustedTypes.createPolicy('MyPromptPolicy', { createHTML: (input) => input })
: null;
function setSafeInnerHTML(element, html) {
if (!element) return;
if (scriptPolicy) {
element.innerHTML = scriptPolicy.createHTML(html);
} else {
element.innerHTML = html;
}
}
function getTranslation(key, replacements = {}) {
let text = translations[currentLang]?.[key] || translations.en[key];
Object.entries(replacements).forEach(([p, v]) => text = text.replace(`{${p}}`, v));
return text;
}
async function determineLanguage() {
const savedLang = await GM_getValue(LANG_STORAGE_KEY);
if (savedLang && translations[savedLang]) {
currentLang = savedLang;
return;
}
const browserLang = (navigator.language || navigator.userLanguage).toLowerCase();
if (browserLang.startsWith('pt')) currentLang = 'pt-BR';
else if (browserLang.startsWith('es')) currentLang = 'es';
else if (browserLang.startsWith('zh')) currentLang = 'zh-CN';
else currentLang = 'en';
}
function waitFor(selector, timeout = 8000) {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) { resolve(el); return; }
const timer = setTimeout(() => { obs.disconnect(); reject(`Timeout esperando por ${selector}`); }, timeout);
const obs = new MutationObserver(() => {
const target = document.querySelector(selector);
if (target) { clearTimeout(timer); obs.disconnect(); resolve(target); }
});
if (document.body) obs.observe(document.body, { childList: true, subtree: true });
else document.addEventListener('DOMContentLoaded', () => obs.observe(document.body, { childList: true, subtree: true }));
});
}
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
async function getAll() { return await GM_getValue(PROMPT_STORAGE_KEY, []); }
async function addItem(item) {
const prompts = await getAll();
prompts.push(item);
await GM_setValue(PROMPT_STORAGE_KEY, prompts);
}
async function update(index, item) {
let prompts = await getAll();
if (prompts[index]) {
prompts[index] = item;
await GM_setValue(PROMPT_STORAGE_KEY, prompts);
}
}
async function remove(index) {
let prompts = await getAll();
prompts.splice(index, 1);
await GM_setValue(PROMPT_STORAGE_KEY, prompts);
}
function createCustomTooltip(button, text, position = 'top') {
let tooltipElement = null;
const showTooltip = () => {
if (tooltipElement) return;
tooltipElement = document.createElement('div');
tooltipElement.className = 'mp-tooltip';
tooltipElement.textContent = text;
document.body.appendChild(tooltipElement);
const btnRect = button.getBoundingClientRect();
const tooltipRect = tooltipElement.getBoundingClientRect();
let top;
const margin = 8;
if (position === 'bottom') {
top = btnRect.bottom + margin;
if (top + tooltipRect.height > window.innerHeight) {
top = btnRect.top - tooltipRect.height - margin;
}
} else {
top = btnRect.top - tooltipRect.height - margin;
if (top < 0) {
top = btnRect.bottom + margin;
}
}
let left = btnRect.left + (btnRect.width / 2) - (tooltipRect.width / 2);
if (left < 0) { left = margin; }
if (left + tooltipRect.width > window.innerWidth) { left = window.innerWidth - tooltipRect.width - margin; }
tooltipElement.style.left = `${left}px`;
tooltipElement.style.top = `${top}px`;
requestAnimationFrame(() => {
tooltipElement.classList.add('visible');
});
};
const hideTooltip = () => {
if (!tooltipElement) return;
const el = tooltipElement;
tooltipElement = null;
el.classList.remove('visible');
setTimeout(() => {
if (document.body.contains(el)) {
document.body.removeChild(el);
}
}, 150);
};
button.addEventListener('mouseenter', showTooltip);
button.addEventListener('mouseleave', hideTooltip);
button.addEventListener('mousedown', hideTooltip);
}
function createChatGPTButton() {
const btn = document.createElement('button');
btn.type = 'button';
btn.setAttribute('data-testid', 'composer-button-prompts');
btn.className = 'composer-btn';
setSafeInnerHTML(btn, `<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M4 5h12M4 10h12M4 15h12" stroke="currentColor" stroke-width="2"/></svg>`);
createCustomTooltip(btn, getTranslation('prompts'));
return btn;
}
function createDeepSeekButton() {
const btn = document.createElement('button');
btn.setAttribute('data-testid', 'composer-button-prompts');
setSafeInnerHTML(btn, `<div class="ds-icon" style="font-size: 17px; width: 17px; height: 17px; color: currentColor; margin-right: 6px;"><svg width="20" height="20" fill="currentColor" viewBox="0 0 20 20"><path d="M4 5h12M4 10h12M4 15h12" stroke="currentColor" stroke-width="2"/></svg></div><span><span class="_6dbc175">${getTranslation('prompt')}</span></span>`);
return btn;
}
function createGoogleStudioButton() {
const styleId = 'my-prompt-gstudio-hover-fix';
if (!document.getElementById(styleId)) {
const styleElement = document.createElement('style');
styleElement.id = styleId;
setSafeInnerHTML(styleElement, `
button[data-testid="composer-button-prompts"]:hover {
background-color: rgba(60, 64, 67, 0.08) !important;
}
@media (prefers-color-scheme: dark) {
button[data-testid="composer-button-prompts"]:hover {
background-color: rgba(232, 234, 237, 0.08) !important;
}
}
`);
document.head.appendChild(styleElement);
}
const btn = document.createElement('button');
btn.setAttribute('data-testid', 'composer-button-prompts');
btn.type = 'button';
btn.style.backgroundColor = 'transparent';
btn.style.border = 'none';
btn.style.boxShadow = 'none';
btn.style.borderRadius = '50%';
btn.style.width = '48px';
btn.style.height = '48px';
btn.style.padding = '0';
btn.style.margin = '0';
btn.style.cursor = 'pointer';
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
btn.style.transition = 'background-color 150ms ease-in-out';
setSafeInnerHTML(btn, `<svg fill="currentColor" style="width: 24px; height: 24px;" viewBox="0 0 100 100"><g><path d="M17.563,30.277h0.012c0,1.245,1.004,2.254,2.246,2.267v0.002h60.359v-0.001c1.248-0.006,2.259-1.018,2.259-2.268h0.01 l0-10.459h0c-0.002-1.251-1.017-2.265-2.269-2.265l0,0H19.821v0c0,0,0,0,0,0c-1.253,0-2.269,1.017-2.269,2.269 c0,0.039,0.01,0.076,0.012,0.115L17.563,30.277z"/><path d="M80.179,42.504L80.179,42.504H19.821v0c0,0,0,0,0,0c-1.253,0-2.269,1.017-2.269,2.269c0,0.039,0.01,0.076,0.012,0.115 l0,10.34h0.012c0,1.245,1.004,2.254,2.246,2.267v0.002h60.359v-0.001c1.248-0.006,2.259-1.018,2.259-2.268h0.01l0-10.459h0 C82.446,43.518,81.431,42.504,80.179,42.504z"/><path d="M80.179,67.454L80.179,67.454H19.821l0,0c0,0,0,0,0,0c-1.253,0-2.269,1.017-2.269,2.269c0,0.039,0.01,0.076,0.012,0.115 l0,10.34h0.012c0,1.245,1.004,2.254,2.246,2.267v0.002h60.359v-0.001c1.248-0.006,2.259-1.019,2.259-2.269h0.01l0-10.459h0 C82.446,68.468,81.431,67.454,80.179,67.454z"/></g></svg>`);
createCustomTooltip(btn, getTranslation('prompts'));
return btn;
}
function createQwenButton() {
const btn = document.createElement('button');
btn.className = 'chat-input-feature-btn';
btn.setAttribute('data-testid', 'composer-button-prompts');
setSafeInnerHTML(btn, `<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="chat-input-feature-btn-icon" style="font-size: 16px;"><path d="M4 5h12M4 10h12M4 15h12" stroke="currentColor" stroke-width="2"/></svg><span class="chat-input-feature-btn-text">${getTranslation('prompt')}</span>`);
return btn;
}
function createZaiButton() {
const btnWrapper = document.createElement('div');
setSafeInnerHTML(btnWrapper, `<button type="button" class="px-2 @xl:px-3 py-1.5 flex gap-1.5 items-center text-sm rounded-lg border transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden bg-transparent dark:text-gray-300 border-[#E5E5E5] dark:border-[#3C3E3F] hover:bg-black/5 dark:hover:bg-white/5"><svg class=" size-4" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 17L3 12L9 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 17L21 12L15 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="hidden @sm:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px] mr-0.5">${getTranslation('prompt')}</span></button>`);
const btn = btnWrapper.firstElementChild;
btn.setAttribute('data-testid', 'composer-button-prompts');
return btn;
}
function createGeminiButton() {
const btn = document.createElement('button');
btn.setAttribute('data-testid', 'composer-button-prompts');
btn.className = 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-primary mat-mdc-tooltip-trigger';
const svgHTML = `<span class="mat-mdc-button-persistent-ripple mdc-icon-button__ripple"></span><span class="mat-icon notranslate" style="display: inline-flex; align-items: center; justify-content: center;"><svg style="width: 24px; height: 24px;" viewBox="0 0 20 20" fill="currentColor"><path d="M4 5h12M4 10h12M4 15h12" stroke="currentColor" stroke-width="2"></path></svg></span><span class="mat-focus-indicator"></span><span class="mat-mdc-button-touch-target"></span><span class="mat-ripple mat-mdc-button-ripple"></span>`;
setSafeInnerHTML(btn, svgHTML);
createCustomTooltip(btn, getTranslation('prompt'), 'bottom');
return btn;
}
function createLmarenaButton() {
const btn = document.createElement('button');
btn.setAttribute('data-testid', 'composer-button-prompts');
btn.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ring-offset-2 focus-visible:ring-offset-surface-primary disabled:pointer-events-none disabled:opacity-50 text-interactive-active border border-border-faint bg-transparent hover:text-interactive-normal active:text-text-tertiary h-8 w-8 p-2 rounded-md active:scale-[0.96] active:transition-transform active:duration-75 transition-colors duration-150 ease-out hover:shadow-sm hover:bg-interactive-normal/10 hover:border-interactive-normal/10';
btn.type = 'button';
setSafeInnerHTML(btn, `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path d="M4 5h12M4 10h12M4 15h12" stroke="currentColor" stroke-width="2"/></svg>`);
createCustomTooltip(btn, getTranslation('prompt'));
return btn;
}
function injectGlobalStyles() {
const styleId='my-prompt-styles';
if (document.getElementById(styleId)) return;
const styleElement=document.createElement('style');
styleElement.id=styleId;
setSafeInnerHTML(styleElement, ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--mp-font-family-base: 'Inter', sans-serif;
--mp-bg-primary: #fff;
--mp-bg-secondary: #f8f9fa;
--mp-bg-tertiary: #f1f3f5;
--mp-bg-overlay: rgba(10, 10, 10, .5);
--mp-text-primary: #212529;
--mp-text-secondary: #495057;
--mp-text-tertiary: #868e96;
--mp-border-primary: #dee2e6;
--mp-border-secondary: #ced4da;
--mp-accent-primary: #7071fc;
--mp-accent-primary-hover: #595ac9;
--mp-accent-yellow: #fab005;
--mp-accent-yellow-hover: #f08c00;
--mp-accent-red: #f03e3e;
--mp-accent-red-hover: #c92a2a;
--mp-shadow-sm: 0 1px 2px rgba(0, 0, 0, .04);
--mp-shadow-md: 0 4px 12px rgba(0, 0, 0, .1);
--mp-shadow-lg: 0 10px 30px rgba(0, 0, 0, .1);
--mp-border-radius-sm: 4px;
--mp-border-radius-md: 8px;
--mp-border-radius-lg: 16px;
--mp-transition-fast: .2s cubic-bezier(.25, 1, .5, 1)
}
@media (prefers-color-scheme:dark) {
:root {
--mp-bg-primary: #212529;
--mp-bg-secondary: #2c2c30;
--mp-bg-tertiary: #343a40;
--mp-bg-overlay: rgba(0, 0, 0, .6);
--mp-text-primary: #f8f9fa;
--mp-text-secondary: #e9ecef;
--mp-text-tertiary: #adb5bd;
--mp-border-primary: #495057;
--mp-border-secondary: #868e96;
--mp-shadow-sm: 0 1px 2px rgba(0, 0, 0, .15);
--mp-shadow-md: 0 4px 12px rgba(0, 0, 0, .25);
--mp-shadow-lg: 0 10px 30px rgba(0, 0, 0, .3)
}
}
.mp-hidden {
display: none !important;
}
.mp-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--mp-bg-overlay);
z-index: 2147483647;
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: opacity var(--mp-transition-fast), visibility var(--mp-transition-fast);
}
.mp-overlay.visible {
opacity: 1;
visibility: visible;
}
.mp-modal-box {
font-family: var(--mp-font-family-base);
background-color: var(--mp-bg-primary);
color: var(--mp-text-primary);
border-radius: var(--mp-border-radius-lg);
padding: 24px;
box-shadow: var(--mp-shadow-lg);
width: min(90vw, 520px);
border: 1px solid var(--mp-border-primary);
transform: scale(.95) translateY(10px);
opacity: 0;
transition: transform var(--mp-transition-fast), opacity var(--mp-transition-fast);
position: relative;
}
.mp-overlay.visible .mp-modal-box {
transform: scale(1) translateY(0);
opacity: 1;
}
.mp-modal-close-btn {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
color: var(--mp-text-tertiary);
font-size: 22px;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
transition: transform .3s ease, color .3s ease, background-color .3s ease;
display: flex;
justify-content: center;
align-items: center;
line-height: 1;
}
.mp-modal-close-btn:hover {
transform: rotate(90deg);
color: var(--mp-accent-red);
background-color: color-mix(in srgb, var(--mp-accent-red) 15%, transparent);
}
.prompt-menu {
position: fixed;
min-width: 320px;
max-width: 420px;
background-color: var(--mp-bg-primary);
border: 1px solid var(--mp-border-primary);
border-radius: var(--mp-border-radius-lg);
box-shadow: var(--mp-shadow-lg);
z-index: 2147483647;
display: flex;
flex-direction: column;
user-select: none;
color: var(--mp-text-primary);
font-family: var(--mp-font-family-base);
overflow: hidden;
opacity: 0;
visibility: hidden;
transform: scale(.95);
transform-origin: top left;
transition: opacity .2s ease, transform .2s ease, visibility 0s linear .2s;
}
.prompt-menu.visible {
opacity: 1;
visibility: visible;
transform: scale(1);
transition-delay: 0s;
}
.prompt-menu-list {
max-height: 220px;
overflow-y: auto;
padding: 4px;
}
.prompt-item-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: var(--mp-border-radius-md);
cursor: pointer;
transition: background-color .15s ease-in-out;
}
.prompt-item-row:hover {
background-color: var(--mp-bg-tertiary);
}
.prompt-title {
font-size: 14px;
font-weight: 500;
flex: 1;
padding-right: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--mp-text-secondary);
}
.prompt-item-row:hover .prompt-title {
color: var(--mp-text-primary);
}
.prompt-actions {
display: flex;
align-items: center;
gap: 4px;
}
.action-btn {
background: 0 0;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 500;
padding: 4px 8px;
border-radius: var(--mp-border-radius-sm);
transition: background-color .15s ease-in-out, color .15s ease-in-out;
font-family: var(--mp-font-family-base);
}
.action-btn.edit {
color: var(--mp-accent-yellow);
}
.action-btn.edit:hover {
background-color: var(--mp-accent-yellow);
color: var(--mp-bg-primary);
}
.action-btn.delete {
color: var(--mp-accent-red);
}
.action-btn.delete:hover {
background-color: var(--mp-accent-red);
color: var(--mp-bg-primary);
}
.menu-footer,
.menu-section {
border-top: 1px solid var(--mp-border-primary);
padding: 4px;
}
.menu-button {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
cursor: pointer;
transition: background-color .15s ease-in-out;
color: var(--mp-text-secondary);
border-radius: var(--mp-border-radius-md);
font-size: 14px;
font-weight: 500;
}
.menu-button:hover {
background-color: var(--mp-bg-tertiary);
color: var(--mp-text-primary);
}
.menu-button svg {
margin-right: 8px;
}
.import-export-container {
display: flex;
}
.import-export-container .menu-button {
flex: 1;
}
.divider {
border-left: 1px solid var(--mp-border-primary);
height: 24px;
align-self: center;
}
.empty-state {
padding: 24px 16px;
text-align: center;
color: var(--mp-text-tertiary);
font-size: 14px;
}
.form-group {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.form-label {
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--mp-text-secondary);
}
.form-input {
background-color: var(--mp-bg-secondary);
color: var(--mp-text-primary);
border: 1px solid var(--mp-border-primary) !important;
border-radius: var(--mp-border-radius-md);
padding: 10px;
width: 100%;
box-sizing: border-box;
transition: border-color .2s, box-shadow .2s;
outline: 0 !important;
font-family: var(--mp-font-family-base);
font-size: 14px
}
.form-textarea {
background-color: var(--mp-bg-secondary);
color: var(--mp-text-primary);
border: 1px solid var(--mp-border-primary) !important;
border-radius: var(--mp-border-radius-md);
padding: 10px;
width: 100%;
box-sizing: border-box;
outline: 0 !important;
font-family: var(--mp-font-family-base);
font-size: 14px;
height: 140px;
resize: vertical;
transition: border-color .2s, box-shadow .2s;
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--mp-accent-primary) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mp-accent-primary) 25%, transparent) !important;
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 24px;
text-align: center;
color: var(--mp-text-primary);
}
.modal-footer {
display: flex;
justify-content: flex-end;
}
.save-button {
padding: 10px 28px;
border-radius: var(--mp-border-radius-md);
background-color: var(--mp-accent-primary);
color: #fff;
border: none;
font-weight: 600;
cursor: pointer;
transition: all .2s ease-in-out;
font-family: var(--mp-font-family-base);
}
.save-button:hover {
background-color: var(--mp-accent-primary-hover);
transform: translateY(-1px);
}
.lang-box {
width: min(90vw, 320px);
}
.lang-buttons-container {
display: flex;
flex-direction: column;
gap: 12px;
}
@keyframes mp-fade-in-up {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.lang-button {
all: unset;
box-sizing: border-box;
display: block;
width: 100%;
padding: 12px 20px;
border-radius: var(--mp-border-radius-md);
background-color: var(--mp-bg-secondary);
color: var(--mp-text-primary);
border: 1px solid var(--mp-border-primary);
font-weight: 500;
cursor: pointer;
text-align: center;
opacity: 0;
animation: mp-fade-in-up .4s ease forwards;
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease;
font-family: var(--mp-font-family-base);
}
.lang-button:hover {
transform: translateY(-3px);
box-shadow: var(--mp-shadow-md);
background-color: var(--mp-bg-tertiary);
}
.lang-button:active {
transform: translateY(0);
transition-duration: .1s;
}
.prompt-menu-list {
scrollbar-width: thin;
scrollbar-color: var(--mp-border-secondary) var(--mp-bg-tertiary);
}
.prompt-menu-list::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
}
.prompt-menu-list::-webkit-scrollbar-track {
background-color: var(--mp-bg-tertiary) !important;
border-radius: 10px !important;
border: none !important;
}
.prompt-menu-list::-webkit-scrollbar-thumb {
background-color: var(--mp-border-secondary) !important;
border-radius: 10px !important;
border: 2px solid var(--mp-bg-primary) !important;
}
.prompt-menu-list::-webkit-scrollbar-thumb:hover {
background-color: var(--mp-text-tertiary) !important;
}
.mp-tooltip {
position: fixed;
z-index: 2147483647;
border-radius: var(--mp-border-radius-sm);
padding: 6px 12px;
pointer-events: none;
white-space: nowrap;
font-family: var(--mp-font-family-base);
font-size: 14px;
font-weight: 500;
background-color: var(--mp-text-primary);
color: var(--mp-bg-primary);
box-shadow: var(--mp-shadow-md);
border: 1px solid var(--mp-bg-tertiary);
opacity: 0;
transform: scale(0.95);
transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.mp-tooltip.visible {
opacity: 1;
transform: scale(1);
}
`);
document.head.appendChild(styleElement);
}
function createPromptMenu() {
const menu = document.createElement('div');
menu.className = 'prompt-menu';
menu.id = 'prompt-menu-container';
return menu;
}
function createPromptModal() {
const overlay = document.createElement('div');
overlay.className = 'mp-overlay mp-hidden';
overlay.id = '__ap_modal_overlay';
const box = document.createElement('div');
box.className = 'mp-modal-box';
box.onclick = e => e.stopPropagation();
const modalContentHTML = `
<button id="__ap_close_prompt" class="mp-modal-close-btn" aria-label="${getTranslation('close')}">✕</button>
<h2 class="modal-title">${getTranslation('newPrompt')}</h2>
<div class="form-group">
<label for="__ap_title" class="form-label">${getTranslation('title')}</label>
<input id="__ap_title" class="form-input" />
</div>
<div class="form-group">
<label for="__ap_text" class="form-label">${getTranslation('text')}</label>
<textarea id="__ap_text" class="form-textarea"></textarea>
</div>
<div class="modal-footer">
<button id="__ap_save" class="save-button">${getTranslation('save')}</button>
</div>
`;
setSafeInnerHTML(box, modalContentHTML);
overlay.appendChild(box);
return overlay;
}
function createLanguageModal() {
const overlay = document.createElement('div');
overlay.className = 'mp-overlay mp-hidden lang-overlay';
overlay.id = '__ap_lang_modal_overlay';
overlay.onclick = () => hideModal(overlay);
const box = document.createElement('div');
box.className = 'mp-modal-box lang-box';
box.onclick = (e) => e.stopPropagation();
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'lang-buttons-container';
Object.keys(translations).forEach((langKey, index) => {
const btn = document.createElement('button');
btn.className = 'lang-button';
btn.textContent = translations[langKey].langName;
btn.style.animationDelay = `${index * 60}ms`;
btn.onclick = async () => {
await GM_setValue(LANG_STORAGE_KEY, langKey);
window.location.reload();
};
buttonsContainer.appendChild(btn);
});
box.appendChild(buttonsContainer);
overlay.appendChild(box);
return overlay;
}
function showModal(modal) {
if (!modal) return;
modal.classList.remove('mp-hidden');
setTimeout(() => modal.classList.add('visible'), 10);
}
function hideModal(modal) {
if (!modal) return;
modal.classList.remove('visible');
setTimeout(() => modal.classList.add('mp-hidden'), 200);
}
function openPromptModal(item = null, index = -1) {
if (!currentModal) return;
const isEditing = !!item;
currentModal.dataset.index = index;
currentModal.querySelector('.modal-title').textContent = isEditing ? getTranslation('editPrompt') : getTranslation('newPrompt');
document.getElementById('__ap_title').value = item?.title || '';
document.getElementById('__ap_text').value = item?.text || '';
showModal(currentModal);
setTimeout(() => document.getElementById('__ap_title').focus(), 100);
}
function moveCursorToEnd(editor) {
setTimeout(() => {
try {
editor.focus();
if (currentPlatform === 'gemini') {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
editor.scrollTop = editor.scrollHeight;
} else if (currentPlatform === 'chatgpt') {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
const scrollContainer = editor.parentElement;
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
} else if (currentPlatform === 'googlestudio') {
const textLength = editor.value.length;
editor.selectionStart = editor.selectionEnd = textLength;
let scrollContainer = editor.parentElement;
let i = 0;
while (scrollContainer && i < 10) {
const style = window.getComputedStyle(scrollContainer);
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
break;
}
scrollContainer = scrollContainer.parentElement;
i++;
}
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
} else {
const textLength = editor.value.length;
editor.selectionStart = editor.selectionEnd = textLength;
editor.scrollTop = editor.scrollHeight;
}
} catch (e) {
}
}, 10);
}
function closeMenu() {
if (currentMenu && currentMenu.classList.contains('visible')) {
currentMenu.classList.remove('visible');
}
}
function positionMenu(menu, button) {
const btnRect = button.getBoundingClientRect();
const menuHeight = menu.offsetHeight;
const menuWidth = menu.offsetWidth;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const margin = 8;
let top, left;
const spaceBelow = viewportHeight - btnRect.bottom - margin;
const spaceAbove = btnRect.top - margin;
if (spaceBelow >= menuHeight) {
top = btnRect.bottom + margin;
} else if (spaceAbove >= menuHeight) {
top = btnRect.top - menuHeight - margin;
} else {
top = Math.max(margin, viewportHeight - menuHeight - margin);
}
const spaceRight = viewportWidth - btnRect.left - margin;
const spaceLeft = btnRect.right - margin;
if (spaceRight >= menuWidth) {
left = btnRect.left;
} else if (spaceLeft >= menuWidth) {
left = btnRect.right - menuWidth;
} else {
left = (viewportWidth - menuWidth) / 2;
}
menu.style.top = `${Math.max(margin, Math.min(top, viewportHeight - menuHeight - margin))}px`;
menu.style.left = `${Math.max(margin, Math.min(left, viewportWidth - menuWidth - margin))}px`;
}
async function refreshMenu() {
if (!currentMenu) return;
const listContainer = document.createElement('div');
listContainer.className = 'prompt-menu-list';
const items = await getAll();
if (!items.length) {
setSafeInnerHTML(listContainer, `<div class="empty-state">${getTranslation('noSavedPrompts')}</div>`);
} else {
items.forEach((p, index) => {
const row = document.createElement('div');
row.className = 'prompt-item-row';
const titleDiv = document.createElement('div');
titleDiv.className = 'prompt-title';
titleDiv.textContent = p.title;
titleDiv.onclick = (e) => { e.stopPropagation(); insertPrompt(p, index); closeMenu(); };
const actionsDiv = document.createElement('div');
actionsDiv.className = 'prompt-actions';
const btnE = document.createElement('button');
btnE.textContent = getTranslation('edit');
btnE.className = 'action-btn edit';
btnE.onclick = (e) => { e.stopPropagation(); openPromptModal(p, index); };
const btnD = document.createElement('button');
btnD.textContent = getTranslation('delete');
btnD.className = 'action-btn delete';
btnD.onclick = (e) => { e.stopPropagation(); if (confirm(getTranslation('confirmDelete', { title: p.title }))) remove(index).then(refreshMenu); };
actionsDiv.appendChild(btnE);
actionsDiv.appendChild(btnD);
row.appendChild(titleDiv);
row.appendChild(actionsDiv);
listContainer.appendChild(row);
});
}
const addSection = document.createElement('div');
addSection.className = 'menu-section';
const addBtn = document.createElement('div');
addBtn.className = 'menu-button';
setSafeInnerHTML(addBtn, `<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>${getTranslation('addPrompt')}`);
addBtn.onclick = (e) => { e.stopPropagation(); openPromptModal(); };
addSection.appendChild(addBtn);
const footer = document.createElement('div');
footer.className = 'menu-footer';
const importExportContainer = document.createElement('div');
importExportContainer.className = 'import-export-container';
const exportBtn = document.createElement('div');
exportBtn.className = 'menu-button';
exportBtn.textContent = getTranslation('export');
exportBtn.onclick = (e) => { e.stopPropagation(); exportPrompts(); };
const importBtn = document.createElement('div');
importBtn.className = 'menu-button';
importBtn.textContent = getTranslation('import');
importBtn.onclick = (e) => { e.stopPropagation(); importPrompts(); };
const divider = document.createElement('div');
divider.className = 'divider';
importExportContainer.appendChild(exportBtn);
importExportContainer.appendChild(divider);
importExportContainer.appendChild(importBtn);
footer.appendChild(importExportContainer);
setSafeInnerHTML(currentMenu, '');
currentMenu.appendChild(listContainer);
currentMenu.appendChild(addSection);
currentMenu.appendChild(footer);
}
function detectPlatform() {
const hostname = window.location.hostname;
if (hostname.includes('chatgpt.com')) return 'chatgpt';
if (hostname.includes('deepseek.com')) return 'deepseek';
if (hostname.includes('aistudio.google.com')) return 'googlestudio';
if (hostname.includes('chat.qwen.ai')) return 'qwen';
if (hostname.includes('chat.z.ai')) return 'zai';
if (hostname.includes('gemini.google.com')) return 'gemini';
if (hostname.includes('lmarena.ai')) return 'lmarena';
return null;
}
async function insertPrompt(promptItem, index) {
const platformSelectors = { chatgpt: '#prompt-textarea', deepseek: 'textarea[placeholder="Message DeepSeek"]', googlestudio: 'ms-autosize-textarea textarea', qwen: 'textarea#chat-input', zai: 'textarea#chat-input', gemini: 'div.ql-editor[contenteditable="true"]', lmarena: 'textarea[name="message"]' };
const editor = document.querySelector(platformSelectors[currentPlatform]);
if (!editor) { alert(getTranslation('editorNotFound', { platform: currentPlatform })); return; }
editor.focus();
setTimeout(() => {
if (currentPlatform === 'gemini') {
let p = editor.querySelector('p') || document.createElement('p');
setSafeInnerHTML(editor, '');
p.textContent = promptItem.text;
editor.appendChild(p);
editor.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
} else if (currentPlatform === 'chatgpt') {
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
if (isFirefox) {
editor.innerHTML = '';
const p = document.createElement('p');
p.textContent = promptItem.text;
editor.appendChild(p);
editor.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
} else {
const dt = new DataTransfer();
dt.setData('text/plain', promptItem.text);
editor.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
}
} else {
const dt = new DataTransfer();
dt.setData('text/plain', promptItem.text);
editor.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
if (editor.value !== promptItem.text) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(editor, promptItem.text);
editor.dispatchEvent(new Event('input', { bubbles: true }));
}
}
moveCursorToEnd(editor);
}, 100);
let prompts = await getAll();
if (index > 0) {
const item = prompts.splice(index, 1)[0];
prompts.unshift(item);
await GM_setValue(PROMPT_STORAGE_KEY, prompts);
}
}
async function exportPrompts() {
const prompts = await getAll();
if(prompts.length===0){alert(getTranslation('noPromptsToExport'));return}
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([JSON.stringify(prompts,null,2)],{type:'application/json'}));
a.download=getTranslation('fileName');
a.click();
URL.revokeObjectURL(a.href);
closeMenu();
}
function importPrompts() {
const input=document.createElement('input');
input.type='file';
input.accept='.json';
input.onchange=e=>{
const file=e.target.files[0];
if(!file)return;
const reader=new FileReader();
reader.onload=async event=>{
try{
const imported=JSON.parse(event.target.result);
if(!Array.isArray(imported))throw new Error("Not an array.");
const current=await getAll();
const newPrompts=imported.map(p=>({title:p.title||'No Title',text:p.text||''}));
await GM_setValue(PROMPT_STORAGE_KEY,[...current,...newPrompts]);
await refreshMenu();
alert(getTranslation('promptsImported',{count:newPrompts.length}));
}catch(err){alert(getTranslation('errorImporting',{error:err.message}))}
};
reader.readAsText(file);
};
input.click();
closeMenu();
}
function cleanup() {
if (currentButton) { currentButton.remove(); currentButton = null; }
if (currentMenu) { currentMenu.remove(); currentMenu = null; }
if (currentModal) { currentModal.remove(); currentModal = null; }
if (languageModal) { languageModal.remove(); languageModal = null; }
isInitialized = false;
}
async function initUI() {
if (pageObserver) pageObserver.disconnect();
cleanup();
currentPlatform = detectPlatform();
if (!currentPlatform) return;
try {
let btn, elementToInsert, insertionPoint, insertionMethod = 'before';
if (currentPlatform === 'chatgpt') {
insertionPoint = await waitFor('div[class*="[grid-area:leading]"]');
insertionPoint.style.display = 'flex';
insertionPoint.style.alignItems = 'center';
btn = createChatGPTButton();
elementToInsert = btn;
insertionMethod = 'append';
} else if (currentPlatform === 'deepseek') {
const container = await waitFor('.ec4f5d61');
insertionPoint = container.querySelector('.bf38813a');
const allButtons = Array.from(container.querySelectorAll('button'));
const refBtn = allButtons.find(b => b.textContent.trim() === 'Search');
if (!refBtn || !insertionPoint) throw new Error('DeepSeek UI reference button "Search" not found.');
btn = createDeepSeekButton();
btn.className = refBtn.className;
elementToInsert = btn;
} else if (currentPlatform === 'googlestudio') {
insertionPoint = await waitFor('ms-add-chunk-menu', 5000).then(el => el.closest('.button-wrapper'));
const wrapper = document.createElement('div');
wrapper.className = 'button-wrapper';
btn = createGoogleStudioButton();
wrapper.appendChild(btn);
elementToInsert = wrapper;
const parent = insertionPoint.closest('.prompt-input-wrapper-container');
if (parent) parent.style.alignItems = 'center';
} else if (currentPlatform === 'qwen') {
insertionPoint = await waitFor('button.websearch_button', 5000);
btn = createQwenButton();
elementToInsert = btn;
insertionMethod = 'after';
const buttonContainer = insertionPoint.parentElement;
if (buttonContainer) {
const qwenPositionObserver = new MutationObserver(() => {
const myButton = buttonContainer.querySelector('button[data-testid="composer-button-prompts"]');
if (myButton && buttonContainer.lastElementChild !== myButton) {
buttonContainer.appendChild(myButton);
}
});
qwenPositionObserver.observe(buttonContainer, { childList: true });
}
} else if (currentPlatform === 'zai') {
const referenceElement = await waitFor('svg path[d="M2.6499 4.48322H13.3166"]', 8000).then(p => p.closest('button'));
if (!referenceElement) throw new Error('Z.ai reference element not found.');
insertionPoint = referenceElement.closest('div.flex.gap-\\[8px\\]');
if (!insertionPoint) throw new Error('Z.ai button container not found.');
btn = createZaiButton();
elementToInsert = btn;
insertionMethod = 'append';
} else if (currentPlatform === 'gemini') {
insertionPoint = await waitFor('uploader', 8000);
btn = createGeminiButton();
elementToInsert = btn;
insertionMethod = 'after';
const wrapper = insertionPoint.parentElement;
if (wrapper) {
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.gap = '3px';
}
} else if (currentPlatform === 'lmarena') {
insertionPoint = await waitFor('div[data-sentry-component="SelectChatModality"]', 8000);
btn = createLmarenaButton();
elementToInsert = btn;
insertionMethod = 'append';
}
if (!btn || !insertionPoint) return;
currentButton = elementToInsert;
const clickable = btn;
if (insertionMethod === 'append') insertionPoint.appendChild(elementToInsert);
else if (insertionMethod === 'before') insertionPoint.parentNode.insertBefore(elementToInsert, insertionPoint);
else insertionPoint.parentNode.insertBefore(elementToInsert, insertionPoint.nextSibling);
currentMenu = createPromptMenu();
currentModal = createPromptModal();
languageModal = createLanguageModal();
document.body.appendChild(currentMenu);
document.body.appendChild(currentModal);
document.body.appendChild(languageModal);
clickable.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
const menu = currentMenu;
if (menu.classList.contains('visible')) {
closeMenu();
return;
}
refreshMenu().then(() => {
positionMenu(menu, clickable);
menu.classList.add('visible');
});
});
currentModal.querySelector('#__ap_save').onclick = async (e) => {
e.stopPropagation();
const index = Number(currentModal.dataset.index) || -1;
const title = document.getElementById('__ap_title').value.trim();
const text = document.getElementById('__ap_text').value.trim();
if (!title || !text) { alert(getTranslation('requiredFields')); return; }
const op = index > -1 ? update(index, { title, text }) : addItem({ title, text });
op.then(() => { hideModal(currentModal); refreshMenu(); });
};
currentModal.querySelector('#__ap_close_prompt').onclick = (e) => {
e.stopPropagation();
hideModal(currentModal);
};
isInitialized = true;
} catch (error) {
cleanup();
} finally {
setupPageObserver();
}
}
const debouncedTryInit = debounce(tryInit, 500);
function setupPageObserver() {
if (pageObserver) pageObserver.disconnect();
pageObserver = new MutationObserver(() => {
if (!document.body.contains(currentButton)) {
debouncedTryInit();
}
});
pageObserver.observe(document.body, { childList: true, subtree: true });
}
function setupGlobalEventListeners() {
document.addEventListener('click', ev => {
if (!currentMenu || !currentButton) return;
if (ev.target.closest('#prompt-menu-container, [data-testid="composer-button-prompts"]')) return;
closeMenu();
});
document.addEventListener('keydown', ev => {
if (ev.key === 'Escape') {
closeMenu();
if (currentModal && currentModal.classList.contains('visible')) hideModal(currentModal);
if (languageModal && languageModal.classList.contains('visible')) hideModal(languageModal);
}
});
window.addEventListener('resize', debounce(() => {
if (currentMenu && currentMenu.classList.contains('visible')) {
positionMenu(currentMenu, currentButton);
}
}, 100));
}
function tryInit() {
if (isInitializing) return;
if (isInitialized && currentButton && document.body.contains(currentButton) && currentPlatform === detectPlatform()) {
return;
}
isInitializing = true;
initUI().finally(() => { isInitializing = false; });
}
async function start() {
await determineLanguage();
injectGlobalStyles();
setupGlobalEventListeners();
GM_registerMenuCommand(getTranslation('languageSettings'), () => {
if (!languageModal) {
languageModal = createLanguageModal();
document.body.appendChild(languageModal);
}
showModal(languageModal);
});
tryInit();
}
start();
})();