Illusion

Illusion(幻觉)是一个跨平台 Prompts 管理工具,支持在以下平台使用:Google AI Studio, ChatGPT, Claude, Grok 和 DeepSeek

  1. // ==UserScript==
  2. // @name Illusion
  3. // @icon https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/image/icons/illusion.png
  4. // @namespace https://github.com/cattail-mutt
  5. // @version 1.5
  6. // @description Illusion(幻觉)是一个跨平台 Prompts 管理工具,支持在以下平台使用:Google AI Studio, ChatGPT, Claude, Grok 和 DeepSeek
  7. // @author Mukai
  8. // @license MIT
  9. // @match https://aistudio.google.com/*
  10. // @match https://chatgpt.com/*
  11. // @match https://claude.ai/*
  12. // @match https://chat.deepseek.com/*
  13. // @match https://grok.com/*
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_getResourceText
  17. // @resource PROMPTS https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/prompt/prompts.json
  18. // @resource THEMES https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/style/themes.json
  19. // @resource CSS https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/style/illusion.css
  20. // @homepage https://greasyfork.org/zh-CN/scripts/527451-%E5%B9%BB%E8%A7%89-illusion
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. const debug = {
  27. enabled: true,
  28. log: (...args) => debug.enabled && console.log('> Illusion 日志:', ...args),
  29. error: (...args) => console.error('> Illusion 错误:', ...args)
  30. };
  31.  
  32. let modalRef = null;
  33. let overlayRef = null;
  34. let savedPrompts = {};
  35.  
  36. const initialPrompts = JSON.parse(GM_getResourceText('PROMPTS'));
  37. const promptsObject = Object.fromEntries(
  38. (initialPrompts || [])
  39. .filter(item => item?.id && item?.value)
  40. .map(item => [item.id, item.value])
  41. );
  42. console.log('PROMPTS解析结果:', promptsObject);
  43.  
  44. const THEMECONFIG = JSON.parse(GM_getResourceText('THEMES'));
  45. debug.log('THEMES解析结果:', THEMECONFIG);
  46.  
  47. function dispatchEvents(element, events) {
  48. events.forEach(eventName => {
  49. const event = eventName === 'input'
  50. ? new InputEvent(eventName, { bubbles: true })
  51. : new Event(eventName, { bubbles: true });
  52. element.dispatchEvent(event);
  53. });
  54. }
  55.  
  56. function createParagraph(line) {
  57. const p = document.createElement('p');
  58. if (line.trim()) {
  59. p.textContent = line;
  60. } else {
  61. p.innerHTML = '<br>';
  62. }
  63. return p;
  64. }
  65.  
  66. const updateProseMirror = (editor, prompt) => { // ChatGPT 和 Claude 均使用了 ProseMirror 库构建富文本编辑器
  67. const paragraphs = Array.from(editor.querySelectorAll('p'));
  68. let currentContent = '';
  69. paragraphs.forEach(p => {
  70. const text = p.textContent.trim();
  71. if (text) {
  72. currentContent += text + '\n';
  73. } else {
  74. currentContent += '\n';
  75. }
  76. });
  77. let newContent = currentContent.trim();
  78. if (newContent) {
  79. newContent += '\n';
  80. }
  81. editor.innerHTML = '';
  82. if (newContent) {
  83. newContent.split('\n').forEach(line => {
  84. editor.appendChild(createParagraph(line));
  85. });
  86. }
  87. const lines = prompt.split('\n');
  88. lines.forEach((line, index) => {
  89. editor.appendChild(createParagraph(line));
  90. if (index < lines.length - 1 && !line.trim()) {
  91. const brP = document.createElement('p');
  92. brP.innerHTML = '<br>';
  93. editor.appendChild(brP);
  94. }
  95. });
  96. dispatchEvents(editor, ['input', 'change']);
  97. };
  98.  
  99. const updateTextArea = async (textarea, prompt) => { // Gemini 和 DeepSeek 使用的均是纯文本输入框 <textarea>
  100. const currentContent = textarea.value;
  101. const newContent = currentContent === ''
  102. ? prompt
  103. : currentContent + "\n" + prompt;
  104. const setter = Object.getOwnPropertyDescriptor(
  105. window.HTMLTextAreaElement.prototype,
  106. "value"
  107. ).set;
  108. setter.call(textarea, newContent);
  109. dispatchEvents(textarea, ['focus', 'input', 'change']);
  110. };
  111.  
  112. const CONFIG = {
  113. maxRetries: 3,
  114. retryDelay: 1000,
  115. initTimeout: 10000,
  116. sync: {
  117. enabled: true, // 是否同步仓库中的 prompts.yaml
  118. blacklist: ['undesired_prompt,e.g. dev', 'undesired_prompt,e.g. graphviz'] // 同步黑名单,其中的键名对应的提示词将不会被同步
  119. },
  120. sites: {
  121. CHATGPT: {
  122. id: 'chatgpt',
  123. icon: 'https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/image/icons/chatgpt.svg',
  124. buttonSize: '48px',
  125. selector: 'div.ProseMirror[contenteditable=true]',
  126. setPrompt: updateProseMirror
  127. },
  128. CLAUDE: { // CSP 限制:用 svg 塞图标
  129. id: 'claude',
  130. icon: `<svg xmlns="http://www.w3.org/2000/svg" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"/></svg>`,
  131. buttonSize: '48px',
  132. selector: 'div.ProseMirror[contenteditable=true]',
  133. setPrompt: updateProseMirror
  134. },
  135. DEEPSEEK: {
  136. id: 'deepseek',
  137. icon: 'https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/image/icons/deepseek.svg',
  138. buttonSize: '48px',
  139. selector: 'textarea[id="chat-input"]',
  140. setPrompt: updateTextArea
  141. },
  142. GEMINI: {
  143. id: 'gemini',
  144. icon: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
  145. buttonSize: '48px',
  146. selector: 'ms-autosize-textarea textarea',
  147. setPrompt: updateTextArea
  148. },
  149. GROK: { // CSP 限制:用 svg 塞图标
  150. id: 'grok',
  151. icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"/></svg>',
  152. buttonSize: '48px',
  153. selector: 'textarea',
  154. setPrompt: updateTextArea
  155. }
  156. }
  157. };
  158.  
  159. function loadExternalCSS() {
  160. const style = document.createElement('style');
  161. style.textContent = GM_getResourceText('CSS');
  162. document.head.appendChild(style);
  163. }
  164.  
  165. function loadsiteTheme() {
  166. const currentSite = getCurrentSite();
  167. const theme = THEMECONFIG[currentSite];
  168. const config = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  169. const root = document.documentElement;
  170. root.style.setProperty('--secondary-bg', theme.secondary);
  171. root.style.setProperty('--text-color', theme.text);
  172. root.style.setProperty('--border-color', theme.border);
  173. root.style.setProperty('--button-bg', theme.button.bg);
  174. root.style.setProperty('--button-hover', theme.button.hover);
  175. root.style.setProperty('--button-size', config.buttonSize);
  176. root.style.setProperty('--panel-bg', theme.panel.bg);
  177. root.style.setProperty('--panel-button-bg', theme.panel.buttonBg);
  178. root.style.setProperty('--panel-button-hover', theme.panel.buttonHover);
  179. }
  180.  
  181. function waitForElement(selector, maxTimeout = CONFIG.initTimeout) {
  182. return new Promise((resolve, reject) => {
  183. const element = document.querySelector(selector);
  184. if(element) {
  185. return resolve(element);
  186. }
  187. let timeout;
  188. const observer = new MutationObserver(() => {
  189. const el = document.querySelector(selector);
  190. if (el) {
  191. observer.disconnect();
  192. clearTimeout(timeout);
  193. resolve(el);
  194. }
  195. });
  196. timeout = setTimeout(() => {
  197. observer.disconnect();
  198. reject(new Error(`超时:使用选择器 ${selector} 寻找元素`));
  199. }, maxTimeout);
  200. observer.observe(document.body, {
  201. childList: true,
  202. subtree: true
  203. });
  204. });
  205. }
  206.  
  207. function getCurrentSite() {
  208. const url = window.location.href;
  209. if(url.includes('aistudio.google.com')) return CONFIG.sites.GEMINI.id;
  210. if(url.includes('chatgpt.com')) return CONFIG.sites.CHATGPT.id;
  211. if(url.includes('claude.ai')) return CONFIG.sites.CLAUDE.id;
  212. if(url.includes('chat.deepseek.com')) return CONFIG.sites.DEEPSEEK.id;
  213. if(url.includes('grok.com')) return CONFIG.sites.GROK.id;
  214. }
  215.  
  216. function createElement(tag, attributes = {}) {
  217. const element = document.createElement(tag);
  218. Object.entries(attributes).forEach(([key, value]) => {
  219. if (key === 'className') {
  220. element.className = value;
  221. } else if (key === 'textContent') {
  222. element.textContent = value;
  223. } else if (key === 'style' && typeof value === 'object') {
  224. Object.assign(element.style, value);
  225. } else if (key === 'onclick') {
  226. element.addEventListener('click', value);
  227. } else if (key.startsWith('data-')) {
  228. element.setAttribute(key, value);
  229. } else if (key === 'html') {
  230. element.innerHTML = value;
  231. } else if (key === 'on' && typeof value === 'object') {
  232. Object.entries(value).forEach(([event, handler]) => {
  233. element.addEventListener(event, handler);
  234. });
  235. } else {
  236. element.setAttribute(key, value);
  237. }
  238. });
  239. return element;
  240. }
  241.  
  242. function filterPromptsByBlacklist(prompts) {
  243. const blacklist = new Set(CONFIG.sync.blacklist);
  244. const filteredPrompts = {};
  245. for (const [id, content] of Object.entries(prompts)) {
  246. if (!blacklist.has(id)) {
  247. filteredPrompts[id] = content;
  248. } else {
  249. debug.log(`提示词 "${id}" 在黑名单中,已过滤`);
  250. }
  251. }
  252. return filteredPrompts;
  253. }
  254.  
  255. function syncPrompts(storedPrompts, initialPromptsObject) {
  256. if (!CONFIG.sync.enabled) {
  257. debug.log('提示词同步功能已禁用');
  258. return storedPrompts;
  259. }
  260. debug.log('提示词库同步中...');
  261. const filteredInitialPrompts = filterPromptsByBlacklist(initialPromptsObject);
  262. let hasNewPrompts = false;
  263. const syncedPrompts = { ...storedPrompts };
  264. for (const [id, content] of Object.entries(filteredInitialPrompts)) {
  265. if (!syncedPrompts[id]) {
  266. debug.log(`发现新提示词: "${id}"`);
  267. syncedPrompts[id] = content;
  268. hasNewPrompts = true;
  269. }
  270. }
  271. if (hasNewPrompts) {
  272. debug.log('同步完成,发现并添加了新的提示词');
  273. GM_setValue('prompts', syncedPrompts);
  274. } else {
  275. debug.log('同步完成,没有发现新的提示词');
  276. }
  277. return syncedPrompts;
  278. }
  279.  
  280. function loadPrompts() {
  281. debug.log('正在载入提示词...');
  282. const storedPrompts = GM_getValue('prompts');
  283. if (!storedPrompts) {
  284. const filteredPrompts = filterPromptsByBlacklist(promptsObject);
  285. savedPrompts = filteredPrompts;
  286. debug.log('未发现 GM 存储中的提示词,将使用初始化时加载的默认提示词:', savedPrompts);
  287. GM_setValue('prompts', savedPrompts);
  288. } else {
  289. debug.log('发现 GM 存储中已有提示词如下', storedPrompts);
  290. savedPrompts = syncPrompts(storedPrompts, promptsObject);
  291. }
  292. return savedPrompts;
  293. }
  294.  
  295. function saveNewPrompt(id, content) {
  296. savedPrompts[id] = content;
  297. GM_setValue('prompts', savedPrompts);
  298. updateDatalist();
  299. debug.log('新的提示词已保存:', id);
  300. }
  301.  
  302. async function setPromptWithRetry(site, prompt, maxRetries = CONFIG.maxRetries) {
  303. return new Promise(async (resolve, reject) => {
  304. let attempts = 0;
  305. const trySetPrompt = async () => {
  306. try {
  307. const config = Object.values(CONFIG.sites).find(s => s.id === site);
  308. if(!config || !config.setPrompt) {
  309. return reject(new Error(`缺少当前站点对应的文本编辑器配置: ${site}`));
  310. }
  311. const editor = document.querySelector(config.selector);
  312. if(!editor) {
  313. throw new Error('在页面上没有找到配置所指定的文本编辑器');
  314. }
  315. await config.setPrompt(editor, prompt);
  316. resolve(true);
  317. } catch (err) {
  318. if (attempts < maxRetries) {
  319. attempts++;
  320. setTimeout(trySetPrompt, CONFIG.retryDelay);
  321. } else {
  322. reject(err);
  323. }
  324. }
  325. };
  326. trySetPrompt();
  327. });
  328. }
  329.  
  330. function initializeUI() {
  331. if (!document.body) {
  332. console.error('找不到 <body>');
  333. setTimeout(initializeUI, 100);
  334. return;
  335. }
  336. loadExternalCSS();
  337. loadsiteTheme();
  338. const button = createButton();
  339. const panel = createPanel();
  340. const { modal, overlay } = createModal();
  341. setupEventListeners(button, panel, modal, overlay);
  342. }
  343.  
  344. function createButton() {
  345. const button = createElement('div', {
  346. className: 'illusion-button',
  347. 'data-tooltip': 'Illusion',
  348. 'data-tooltip-position': 'left',
  349. 'aria-label': 'Illusion'
  350. });
  351. const currentSite = getCurrentSite();
  352. const config = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  353. if(config.icon.startsWith('http')) {
  354. const img = createElement('img', {
  355. src: config.icon,
  356. width: '24',
  357. height: '24',
  358. style: {
  359. pointerEvents: 'none'
  360. }
  361. });
  362. button.appendChild(img);
  363. } else {
  364. button.innerHTML = config.icon;
  365. const svg = button.querySelector('svg');
  366. if(svg) {
  367. svg.style.width = '24px';
  368. svg.style.height = '24px';
  369. svg.style.pointerEvents = 'none';
  370. }
  371. }
  372. document.body.appendChild(button);
  373. makeDraggable(button);
  374. return button;
  375. }
  376.  
  377. function createPanel() {
  378. const panel = createElement('div', {
  379. className: 'illusion-panel'
  380. });
  381. const title = createElement('div', {
  382. className: 'panel-title',
  383. textContent: 'Illusion'
  384. });
  385. panel.appendChild(title);
  386. const inputGroup = createElement('div', {
  387. className: 'input-group'
  388. });
  389. const input = createElement('input', {
  390. className: 'prompt-input',
  391. type: 'text',
  392. list: 'prompt-options',
  393. placeholder: '查找 Prompt'
  394. });
  395. const datalist = createElement('datalist', {
  396. id: 'prompt-options'
  397. });
  398. Object.keys(savedPrompts).forEach(id => {
  399. const option = createElement('option', {
  400. value: id
  401. });
  402. datalist.appendChild(option);
  403. });
  404. inputGroup.appendChild(input);
  405. inputGroup.appendChild(datalist);
  406. panel.appendChild(inputGroup);
  407. const buttonGroup = createElement('div', {
  408. className: 'button-group'
  409. });
  410. const newButton = createElement('button', {
  411. className: 'panel-button',
  412. textContent: 'New',
  413. 'data-action': 'new',
  414. onclick: () => {
  415. const modal = document.querySelector('.modal');
  416. const overlay = document.querySelector('.modal-overlay');
  417. if (modal && overlay) {
  418. showNewPromptModal(modal, overlay);
  419. } else {
  420. debug.error('找不到模态框/遮罩层');
  421. }
  422. }
  423. });
  424. buttonGroup.appendChild(newButton);
  425. const manageButton = createElement('button', {
  426. className: 'panel-button',
  427. textContent: 'Manage',
  428. 'data-action': 'manage',
  429. onclick: () => {
  430. const modal = document.querySelector('.modal');
  431. const overlay = document.querySelector('.modal-overlay');
  432. if (modal && overlay) {
  433. showManagePromptsModal(modal, overlay);
  434. } else {
  435. debug.error('找不到模态框/遮罩层');
  436. }
  437. }
  438. });
  439. buttonGroup.appendChild(manageButton);
  440. panel.appendChild(buttonGroup);
  441. document.body.appendChild(panel);
  442. return panel;
  443. }
  444.  
  445. function createModal() {
  446. overlayRef = createElement('div', {
  447. className: 'modal-overlay'
  448. });
  449. document.body.appendChild(overlayRef);
  450. modalRef = createElement('div', {
  451. className: 'modal',
  452. role: 'dialog',
  453. 'aria-modal': 'true'
  454. });
  455. document.body.appendChild(modalRef);
  456. return { modal: modalRef, overlay: overlayRef };
  457. }
  458.  
  459. function createModalFooter(buttonConfigs, modal, overlay) {
  460. const footer = createElement('div', { className: 'modal-footer' });
  461. buttonConfigs.forEach(config => {
  462. const btn = createElement('button', {
  463. className: 'panel-button',
  464. textContent: config.text,
  465. onclick: () => {
  466. config.onClick && config.onClick(modal, overlay);
  467. }
  468. });
  469. footer.appendChild(btn);
  470. });
  471. return footer;
  472. }
  473.  
  474. function openModal(modal, overlay, container) {
  475. modal.textContent = '';
  476. modal.appendChild(container);
  477. modal.classList.add('visible');
  478. overlay.classList.add('visible');
  479. }
  480.  
  481. function hideModal(modal, overlay) {
  482. modal.classList.remove('visible');
  483. overlay.classList.remove('visible');
  484. }
  485.  
  486. function getEventPosition(e) {
  487. if (e.touches && e.touches.length) {
  488. return { x: e.touches[0].clientX, y: e.touches[0].clientY };
  489. }
  490. return { x: e.clientX, y: e.clientY };
  491. }
  492.  
  493. function makeDraggable(button) {
  494. let isDragging = false;
  495. let startX, startY;
  496. let initialX, initialY;
  497. let lastValidX, lastValidY;
  498. let dragThrottle;
  499.  
  500. function setButtonPosition(x, y) {
  501. button.style.left = x + 'px';
  502. button.style.top = y + 'px';
  503. GM_setValue('buttonPosition', { x, y });
  504. }
  505.  
  506. function dragStart(e) {
  507. const pos = getEventPosition(e);
  508. startX = pos.x;
  509. startY = pos.y;
  510. const rect = button.getBoundingClientRect();
  511. initialX = rect.left;
  512. initialY = rect.top;
  513. isDragging = true;
  514. button.classList.remove('docked');
  515. button.classList.add('dragging');
  516. }
  517.  
  518. function dragEnd() {
  519. if (!isDragging) return;
  520. isDragging = false;
  521. button.classList.remove('dragging');
  522. const rect = button.getBoundingClientRect();
  523. const viewportWidth = window.innerWidth;
  524. const threshold = viewportWidth * 0.3;
  525. if (rect.left > viewportWidth - threshold) {
  526. setButtonPosition(viewportWidth - rect.width, rect.top);
  527. button.classList.add('docked');
  528. } else if (rect.left < threshold) {
  529. setButtonPosition(0, rect.top);
  530. button.classList.add('docked');
  531. }
  532. }
  533.  
  534. function drag(e) {
  535. if (!isDragging) return;
  536. e.preventDefault();
  537. if (dragThrottle) return;
  538. dragThrottle = setTimeout(() => {
  539. dragThrottle = null;
  540. }, 16);
  541. const { x: currentX, y: currentY } = getEventPosition(e);
  542. const deltaX = currentX - startX;
  543. const deltaY = currentY - startY;
  544.  
  545. requestAnimationFrame(() => {
  546. setButtonPosition(initialX + deltaX, initialY + deltaY);
  547. lastValidX = initialX + deltaX;
  548. lastValidY = initialY + deltaY;
  549. });
  550. }
  551.  
  552. button.addEventListener('touchstart', dragStart, { passive: false });
  553. button.addEventListener('touchend', dragEnd, { passive: false });
  554. button.addEventListener('touchmove', drag, { passive: false });
  555. button.addEventListener('mousedown', dragStart);
  556. document.addEventListener('mousemove', drag);
  557. document.addEventListener('mouseup', dragEnd);
  558. window.addEventListener('resize', () => {
  559. if (lastValidX !== undefined && lastValidY !== undefined) {
  560. setButtonPosition(lastValidX, lastValidY);
  561. }
  562. });
  563. const savedPosition = GM_getValue('buttonPosition');
  564. if (savedPosition) {
  565. setButtonPosition(savedPosition.x, savedPosition.y);
  566. } else {
  567. setButtonPosition(
  568. window.innerWidth - button.offsetWidth - 20,
  569. window.innerHeight / 2 - button.offsetHeight / 2
  570. );
  571. }
  572. }
  573.  
  574. function createModalContainer(title, contentElement, footerElement) {
  575. const container = createElement('div');
  576. const header = createElement('div', {
  577. className: 'modal-header',
  578. textContent: title
  579. });
  580. container.appendChild(header);
  581. container.appendChild(contentElement);
  582. container.appendChild(footerElement);
  583. return container;
  584. }
  585.  
  586. function createFormGroup(labelText, inputOptions) {
  587. const group = createElement('div', { className: 'form-group' });
  588. const label = createElement('label', {
  589. className: 'form-label',
  590. textContent: labelText
  591. });
  592. let input;
  593. if (inputOptions.type === 'textarea') {
  594. input = createElement('textarea', inputOptions);
  595. } else {
  596. input = createElement('input', inputOptions);
  597. }
  598. group.appendChild(label);
  599. group.appendChild(input);
  600. return { group, input };
  601. }
  602.  
  603. function createPromptModal({ title, promptId, promptContent, isEditable, onSave, modal, overlay }) {
  604. const content = createElement('div', { className: 'modal-content' });
  605. const { group: idGroup, input: idInput } = createFormGroup('Prompt ID', {
  606. className: 'form-input',
  607. type: 'text',
  608. value: promptId || '',
  609. placeholder: '为提示词命名'
  610. });
  611. if (promptId && !isEditable) {
  612. idInput.disabled = true;
  613. }
  614. content.appendChild(idGroup);
  615. const { group: contentGroup, input: contentInput } = createFormGroup('Prompt Content', {
  616. className: 'form-textarea',
  617. type: 'textarea',
  618. value: promptContent || '',
  619. placeholder: '输入提示词的内容'
  620. });
  621. contentInput.value = promptContent || '';
  622. content.appendChild(contentGroup);
  623. const footerButtons = [
  624. {
  625. text: 'Cancel',
  626. onClick: (modal, overlay) => hideModal(modal, overlay)
  627. },
  628. {
  629. text: promptId ? 'Update' : 'Save',
  630. onClick: () => {
  631. const idVal = idInput.value.trim();
  632. const contentVal = contentInput.value.trim();
  633. onSave(idVal, contentVal);
  634. }
  635. }
  636. ];
  637. const footer = createModalFooter(footerButtons, modal, overlay);
  638. const container = createModalContainer(title, content, footer);
  639. openModal(modal, overlay, container);
  640. setTimeout(() => contentInput.focus(), 100);
  641. }
  642.  
  643. function showNewPromptModal(modal, overlay) {
  644. createPromptModal({
  645. title: 'New Prompt',
  646. promptId: '',
  647. promptContent: '',
  648. isEditable: true,
  649. onSave: (id, content) => {
  650. if (id && content) {
  651. try {
  652. saveNewPrompt(id, content);
  653. hideModal(modal, overlay);
  654. } catch (err) {
  655. debug.error('提示词保存失败:', err);
  656. alert('保存失败,请重试');
  657. }
  658. } else {
  659. alert('请填写所有必填字段');
  660. }
  661. },
  662. modal,
  663. overlay
  664. });
  665. }
  666. function showEditPromptModal(modal, overlay, id, content) {
  667. createPromptModal({
  668. title: 'Prompt Editing',
  669. promptId: id,
  670. promptContent: content,
  671. isEditable: false,
  672. onSave: (id, newContent) => {
  673. if (newContent) {
  674. savedPrompts[id] = newContent;
  675. GM_setValue('prompts', savedPrompts);
  676. hideModal(modal, overlay);
  677. showManagePromptsModal(modal, overlay);
  678. updateDatalist();
  679. } else {
  680. alert('内容不能为空');
  681. }
  682. },
  683. modal,
  684. overlay
  685. });
  686. }
  687.  
  688. function showManagePromptsModal(modal, overlay) {
  689. const container = createElement('div');
  690. const header = createElement('div', {
  691. className: 'modal-header',
  692. textContent: 'Manage Prompts'
  693. });
  694. container.appendChild(header);
  695. const content = createElement('div', {
  696. className: 'modal-content'
  697. });
  698. Object.entries(savedPrompts).forEach(([id, promptContent]) => {
  699. const promptGroup = createElement('div', {
  700. className: 'form-group',
  701. style: {
  702. display: 'flex',
  703. alignItems: 'center',
  704. justifyContent: 'space-between',
  705. padding: '8px',
  706. borderBottom: '1px solid #eee'
  707. }
  708. });
  709. const promptId = createElement('div', {
  710. className: 'form-label',
  711. style: { margin: '0', flex: '1' },
  712. textContent: id
  713. });
  714. const buttonGroup = createElement('div', {
  715. className: 'button-group',
  716. style: { marginLeft: '16px' }
  717. });
  718. const editButton = createElement('button', {
  719. className: 'panel-button',
  720. textContent: 'Edit',
  721. onclick: () => {
  722. showEditPromptModal(modal, overlay, id, promptContent);
  723. }
  724. });
  725. const deleteButton = createElement('button', {
  726. className: 'panel-button',
  727. textContent: 'Delete',
  728. onclick: () => {
  729. if (confirm(`确定要删除 "${id}" 吗?`)) {
  730. delete savedPrompts[id];
  731. GM_setValue('prompts', savedPrompts);
  732. promptGroup.remove();
  733. updateDatalist();
  734. }
  735. }
  736. });
  737. buttonGroup.appendChild(editButton);
  738. buttonGroup.appendChild(deleteButton);
  739. promptGroup.appendChild(promptId);
  740. promptGroup.appendChild(buttonGroup);
  741. content.appendChild(promptGroup);
  742. });
  743. container.appendChild(content);
  744. const footer = createElement('div', {
  745. className: 'modal-footer'
  746. });
  747. const closeButton = createElement('button', {
  748. className: 'panel-button',
  749. textContent: 'Close',
  750. onclick: () => {
  751. hideModal(modal, overlay);
  752. }
  753. });
  754. footer.appendChild(closeButton);
  755. container.appendChild(footer);
  756. openModal(modal, overlay, container);
  757. }
  758.  
  759. function updateDatalist() {
  760. const datalist = document.getElementById('prompt-options');
  761. if (!datalist) return;
  762. debug.log('最新的提示词列表:', savedPrompts);
  763. datalist.textContent = '';
  764. Object.keys(savedPrompts).forEach(id => {
  765. const option = createElement('option', { value: id });
  766. datalist.appendChild(option);
  767. });
  768. }
  769.  
  770. function setupEventListeners(button, panel, modal, overlay) {
  771. panel.addEventListener('click', (e) => {
  772. e.stopPropagation();
  773. });
  774.  
  775. button.addEventListener('click', (e) => {
  776. if(!e.target.classList.contains('dragging')) {
  777. e.stopPropagation();
  778. panel.classList.toggle('visible');
  779. }
  780. });
  781.  
  782. document.addEventListener('click', () => {
  783. if (panel.classList.contains('visible')) {
  784. panel.classList.remove('visible');
  785. }
  786. });
  787.  
  788. panel.addEventListener('click', (e) => {
  789. const btn = e.target.closest('button[data-action]');
  790. if (!btn) return;
  791. const action = btn.dataset.action;
  792. try {
  793. if (action === 'new') {
  794. showNewPromptModal(modalRef, overlayRef);
  795. } else if (action === 'manage') {
  796. showManagePromptsModal(modalRef, overlayRef);
  797. }
  798. } catch (error) {
  799. debug.error('无效的点击操作或按钮绑定的操作异常', error);
  800. }
  801. });
  802.  
  803. const promptInput = panel.querySelector('.prompt-input');
  804.  
  805. promptInput.addEventListener('change', async (e) => {
  806. const selectedValue = e.target.value.trim();
  807. const promptContent = savedPrompts[selectedValue];
  808. if (promptContent) {
  809. debug.log('已选择提示词:', selectedValue);
  810. try {
  811. const site = getCurrentSite();
  812. const success = await setPromptWithRetry(site, promptContent);
  813. if (!success) {
  814. alert('提示词输入失败,请重试');
  815. }
  816. } catch(err) {
  817. alert('提示词输入过程中出现如下错误:' + err.message);
  818. }
  819. }
  820. e.target.value = '';
  821. });
  822.  
  823. overlay.addEventListener('click', () => {
  824. hideModal(modal, overlay);
  825. });
  826.  
  827. document.addEventListener('keydown', (e) => {
  828. if (e.key === 'Escape' && modal.classList.contains('visible')) {
  829. hideModal(modal, overlay);
  830. }
  831. });
  832. }
  833.  
  834. async function initialize() {
  835. debug.log('正在初始化...');
  836. try {
  837. const savedSyncConfig = GM_getValue('syncConfig');
  838. if (savedSyncConfig) {
  839. CONFIG.sync = { ...CONFIG.sync, ...savedSyncConfig };
  840. }
  841. const currentSite = getCurrentSite();
  842. savedPrompts = loadPrompts();
  843. const siteConfig = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  844. if (!siteConfig) {
  845. debug.error('缺少当前站点的相关配置');
  846. return;
  847. }
  848. const editorSelector = siteConfig.selector;
  849. try {
  850. await waitForElement(editorSelector);
  851. } catch(err) {
  852. debug.error('找不到与配置匹配的文本编辑器元素:', err);
  853. return;
  854. }
  855. initializeUI();
  856. } catch (error) {
  857. debug.error('初始化过程中发生错误:', error);
  858. }
  859. }
  860.  
  861. if (document.readyState === 'loading') {
  862. document.addEventListener('DOMContentLoaded', initialize);
  863. } else {
  864. initialize();
  865. }
  866. })();