幻觉(Illusion)

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

目前为 2025-02-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 幻觉(Illusion)
  3. // @icon https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/icons/illusion.png
  4. // @namespace LINUX_DO
  5. // @version 1.2
  6. // @description 幻觉(Illusion)是一个精简的跨平台 Prompts 管理工具,支持在以下 AI 平台使用:Google AI Studio, OpenAI ChatGPT, Anthropic Claude 和 DeepSeek Chat。
  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. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_getResourceText
  16. // @resource PROMPTS https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/config/prompts.yaml
  17. // @resource THEMES https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/config/themes.json
  18. // @resource CSS https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/styles/illusion.css
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js
  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. let modalRef = null;
  27. let overlayRef = null;
  28. let savedPrompts = {};
  29. window.onerror = function(msg, url, line, col, error) {
  30. console.error('[Illusion]错误:', {msg, url, line, col, error});
  31. return false;
  32. };
  33. const debug = {
  34. enabled: true,
  35. log: (...args) => debug.enabled && console.log('[Illusion]日志:', ...args),
  36. error: (...args) => console.error('[Illusion]错误:', ...args),
  37. warn: (...args) => console.warn('[Illusion]警告:', ...args),
  38. trace: (...args) => debug.enabled && console.trace('[Illusion]追踪:', ...args)
  39. };
  40.  
  41. const initialPrompts = jsyaml.load(GM_getResourceText('PROMPTS'));
  42. const THEMECONFIG = JSON.parse(GM_getResourceText('THEMES'));
  43. debug.log('[Illusion]日志: PROMPTS解析结果:', initialPrompts);
  44. debug.log('[Illusion]日志: THEMES解析结果:', THEMECONFIG);
  45.  
  46. function dispatchEvents(element, events) {
  47. events.forEach(eventName => {
  48. const event = eventName === 'input'
  49. ? new InputEvent(eventName, { bubbles: true })
  50. : new Event(eventName, { bubbles: true });
  51. element.dispatchEvent(event);
  52. });
  53. }
  54.  
  55. // ChatGPT 和 Claude 均使用了 ProseMirror 库构建富文本编辑器
  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) => {
  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. // Gemini 和 DeepSeek 使用的均是纯文本输入框 <textarea>
  100. // DeepSeek 不支持对 textarea.value 直接更新
  101. const updateTextArea = async (textarea, prompt) => {
  102. const currentContent = textarea.value;
  103. const newContent = currentContent === ''
  104. ? prompt
  105. : currentContent + "\n" + prompt;
  106. const setter = Object.getOwnPropertyDescriptor(
  107. window.HTMLTextAreaElement.prototype,
  108. "value"
  109. ).set;
  110. setter.call(textarea, newContent);
  111. dispatchEvents(textarea, ['focus', 'input', 'change']);
  112. };
  113.  
  114. const CONFIG = {
  115. debugEnabled: true,
  116. maxRetries: 3,
  117. retryDelay: 1000,
  118. initTimeout: 10000,
  119. eventDelay: 50,
  120. eventTimeout: 1000,
  121. sites: {
  122. CHATGPT: {
  123. id: 'chatgpt',
  124. icon: 'https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/icons/chatgpt.svg',
  125. buttonSize: '48px',
  126. selector: 'div.ProseMirror[contenteditable=true]',
  127. setPrompt: updateProseMirror
  128. },
  129. CLAUDE: {
  130. id: 'claude',
  131. 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>`,
  132. buttonSize: '48px',
  133. selector: 'div.ProseMirror[contenteditable=true]',
  134. setPrompt: updateProseMirror
  135. },
  136. GEMINI: {
  137. id: 'gemini',
  138. icon: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
  139. buttonSize: '48px',
  140. selector: 'textarea[aria-label="Type something"]',
  141. setPrompt: updateTextArea
  142. },
  143. DEEPSEEK: {
  144. id: 'deepseek',
  145. icon: 'https://raw.githubusercontent.com/cattail-mutt/Illusion/refs/heads/main/resources/icons/deepseek.svg',
  146. buttonSize: '48px',
  147. selector: 'textarea[id="chat-input"]',
  148. setPrompt: updateTextArea
  149. }
  150. }
  151. };
  152.  
  153. function loadExternalCSS() {
  154. const style = document.createElement('style');
  155. style.textContent = GM_getResourceText('CSS');
  156. document.head.appendChild(style);
  157. }
  158.  
  159. function applyThemeVariables() {
  160. const currentSite = getCurrentSite();
  161. const theme = THEMECONFIG[currentSite];
  162. const config = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  163. const root = document.documentElement;
  164. root.style.setProperty('--button-size', config.buttonSize);
  165. root.style.setProperty('--button-bg', theme.button.bg);
  166. root.style.setProperty('--border-color', theme.border);
  167. root.style.setProperty('--button-hover', theme.button.hover);
  168. root.style.setProperty('--panel-bg', theme.panel.bg);
  169. root.style.setProperty('--panel-button-bg', theme.panel.buttonBg);
  170. root.style.setProperty('--panel-button-hover', theme.panel.buttonHover);
  171. root.style.setProperty('--text-color', theme.text);
  172. root.style.setProperty('--secondary-bg', theme.secondary);
  173. }
  174.  
  175. function waitForElement(selector, maxTimeout = CONFIG.initTimeout) {
  176. return new Promise((resolve, reject) => {
  177. const element = document.querySelector(selector);
  178. if(element) {
  179. return resolve(element);
  180. }
  181. let timeout;
  182. const observer = new MutationObserver(() => {
  183. const el = document.querySelector(selector);
  184. if (el) {
  185. observer.disconnect();
  186. clearTimeout(timeout);
  187. resolve(el);
  188. }
  189. });
  190. timeout = setTimeout(() => {
  191. observer.disconnect();
  192. reject(new Error(`Timeout waiting for ${selector}`));
  193. }, maxTimeout);
  194. observer.observe(document.body, {
  195. childList: true,
  196. subtree: true
  197. });
  198. });
  199. }
  200.  
  201. function getCurrentSite() {
  202. const url = window.location.href;
  203. if(url.includes('aistudio.google.com')) return CONFIG.sites.GEMINI.id;
  204. if(url.includes('chatgpt.com')) return CONFIG.sites.CHATGPT.id;
  205. if(url.includes('claude.ai')) return CONFIG.sites.CLAUDE.id;
  206. if(url.includes('chat.deepseek.com')) return CONFIG.sites.DEEPSEEK.id;
  207. }
  208.  
  209. function createElement(tag, attributes = {}) {
  210. const element = document.createElement(tag);
  211. Object.entries(attributes).forEach(([key, value]) => {
  212. if (key === 'className') {
  213. element.className = value;
  214. } else if (key === 'textContent') {
  215. element.textContent = value;
  216. } else if (key === 'style' && typeof value === 'object') {
  217. Object.assign(element.style, value);
  218. } else if (key === 'onclick') {
  219. element.addEventListener('click', value);
  220. } else if (key.startsWith('data-')) {
  221. element.setAttribute(key, value);
  222. } else if (key === 'html') {
  223. element.innerHTML = value;
  224. } else if (key === 'on' && typeof value === 'object') {
  225. Object.entries(value).forEach(([event, handler]) => {
  226. element.addEventListener(event, handler);
  227. });
  228. } else {
  229. element.setAttribute(key, value);
  230. }
  231. });
  232. return element;
  233. }
  234.  
  235. function loadPrompts() {
  236. const storedPrompts = GM_getValue('prompts');
  237. if (!storedPrompts) {
  238. savedPrompts = initialPrompts;
  239. GM_setValue('prompts', savedPrompts);
  240. } else {
  241. savedPrompts = storedPrompts;
  242. }
  243. return savedPrompts;
  244. }
  245.  
  246. function saveNewPrompt(id, content) {
  247. savedPrompts[id] = content;
  248. GM_setValue('prompts', savedPrompts);
  249. updateDatalist();
  250. debug.log('New prompt saved:', id);
  251. }
  252.  
  253. async function setPromptWithRetry(site, prompt, maxRetries = CONFIG.maxRetries) {
  254. return new Promise(async (resolve, reject) => {
  255. let attempts = 0;
  256. const trySetPrompt = async () => {
  257. try {
  258. const config = Object.values(CONFIG.sites).find(s => s.id === site);
  259. if(!config || !config.setPrompt) {
  260. return reject(new Error(`No prompt setter configured for site: ${site}`));
  261. }
  262. const editor = document.querySelector(config.selector);
  263. if(!editor) {
  264. throw new Error('Editor element not found');
  265. }
  266. await config.setPrompt(editor, prompt);
  267. resolve(true);
  268. } catch (err) {
  269. if (attempts < maxRetries) {
  270. attempts++;
  271. setTimeout(trySetPrompt, CONFIG.retryDelay);
  272. } else {
  273. reject(err);
  274. }
  275. }
  276. };
  277. trySetPrompt();
  278. });
  279. }
  280.  
  281. function initializeUI() {
  282. loadExternalCSS();
  283. applyThemeVariables();
  284. const button = createButton();
  285. const panel = createPanel();
  286. const { modal, overlay } = createModal();
  287. setupEventListeners(button, panel, modal, overlay);
  288. }
  289.  
  290. function createButton() {
  291. const button = createElement('div', {
  292. className: 'illusion-button',
  293. 'data-tooltip': 'Illusion',
  294. 'data-tooltip-position': 'left',
  295. 'aria-label': 'Illusion'
  296. });
  297. const currentSite = getCurrentSite();
  298. const config = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  299. if(config.icon.startsWith('http')) {
  300. const img = createElement('img', {
  301. src: config.icon,
  302. width: '24',
  303. height: '24',
  304. style: {
  305. pointerEvents: 'none'
  306. }
  307. });
  308. button.appendChild(img);
  309. } else {
  310. button.innerHTML = config.icon;
  311. const svg = button.querySelector('svg');
  312. if(svg) {
  313. svg.style.width = '24px';
  314. svg.style.height = '24px';
  315. svg.style.pointerEvents = 'none';
  316. }
  317. }
  318. document.body.appendChild(button);
  319. makeDraggable(button);
  320. debug.log('Button created');
  321. return button;
  322. }
  323.  
  324. function createPanel() {
  325. const panel = createElement('div', {
  326. className: 'illusion-panel'
  327. });
  328. const title = createElement('div', {
  329. className: 'panel-title',
  330. textContent: 'Illusion'
  331. });
  332. panel.appendChild(title);
  333. const inputGroup = createElement('div', {
  334. className: 'input-group'
  335. });
  336. const input = createElement('input', {
  337. className: 'prompt-input',
  338. type: 'text',
  339. list: 'prompt-options',
  340. placeholder: '查找 Prompt'
  341. });
  342. const datalist = createElement('datalist', {
  343. id: 'prompt-options'
  344. });
  345. Object.keys(savedPrompts).forEach(id => {
  346. const option = createElement('option', {
  347. value: id
  348. });
  349. datalist.appendChild(option);
  350. });
  351. inputGroup.appendChild(input);
  352. inputGroup.appendChild(datalist);
  353. panel.appendChild(inputGroup);
  354. const buttonGroup = createElement('div', {
  355. className: 'button-group'
  356. });
  357. const newButton = createElement('button', {
  358. className: 'panel-button',
  359. textContent: 'New Prompt',
  360. 'data-action': 'new',
  361. onclick: () => {
  362. debug.log('New prompt button clicked');
  363. const modal = document.querySelector('.modal');
  364. const overlay = document.querySelector('.modal-overlay');
  365. if (modal && overlay) {
  366. showNewPromptModal(modal, overlay);
  367. } else {
  368. debug.error('Modal or overlay elements not found');
  369. }
  370. }
  371. });
  372. buttonGroup.appendChild(newButton);
  373. const manageButton = createElement('button', {
  374. className: 'panel-button',
  375. textContent: 'Manage',
  376. 'data-action': 'manage',
  377. onclick: () => {
  378. debug.log('Manage button clicked');
  379. const modal = document.querySelector('.modal');
  380. const overlay = document.querySelector('.modal-overlay');
  381. if (modal && overlay) {
  382. showManagePromptsModal(modal, overlay);
  383. } else {
  384. debug.error('Modal or overlay elements not found');
  385. }
  386. }
  387. });
  388. buttonGroup.appendChild(manageButton);
  389. panel.appendChild(buttonGroup);
  390. document.body.appendChild(panel);
  391. debug.log('Panel created');
  392. return panel;
  393. }
  394.  
  395. function createModal() {
  396. overlayRef = createElement('div', {
  397. className: 'modal-overlay'
  398. });
  399. document.body.appendChild(overlayRef);
  400. modalRef = createElement('div', {
  401. className: 'modal',
  402. role: 'dialog',
  403. 'aria-modal': 'true'
  404. });
  405. document.body.appendChild(modalRef);
  406. return { modal: modalRef, overlay: overlayRef };
  407. }
  408.  
  409. function createModalFooter(buttonConfigs, modal, overlay) {
  410. const footer = createElement('div', { className: 'modal-footer' });
  411. buttonConfigs.forEach(config => {
  412. const btn = createElement('button', {
  413. className: 'panel-button',
  414. textContent: config.text,
  415. onclick: () => {
  416. config.onClick && config.onClick(modal, overlay);
  417. }
  418. });
  419. footer.appendChild(btn);
  420. });
  421. return footer;
  422. }
  423.  
  424. function openModal(modal, overlay, container) {
  425. modal.textContent = '';
  426. modal.appendChild(container);
  427. modal.classList.add('visible');
  428. overlay.classList.add('visible');
  429. }
  430.  
  431. function hideModal(modal, overlay) {
  432. modal.classList.remove('visible');
  433. overlay.classList.remove('visible');
  434. }
  435.  
  436. function getEventPosition(e) {
  437. if (e.touches && e.touches.length) {
  438. return { x: e.touches[0].clientX, y: e.touches[0].clientY };
  439. }
  440. return { x: e.clientX, y: e.clientY };
  441. }
  442.  
  443. function makeDraggable(button) {
  444. let isDragging = false;
  445. let startX, startY;
  446. let initialX, initialY;
  447. let lastValidX, lastValidY;
  448. let dragThrottle;
  449.  
  450. function setButtonPosition(x, y) {
  451. button.style.left = x + 'px';
  452. button.style.top = y + 'px';
  453. GM_setValue('buttonPosition', { x, y });
  454. }
  455.  
  456. function dragStart(e) {
  457. const pos = getEventPosition(e);
  458. startX = pos.x;
  459. startY = pos.y;
  460. const rect = button.getBoundingClientRect();
  461. initialX = rect.left;
  462. initialY = rect.top;
  463. isDragging = true;
  464. button.classList.remove('docked');
  465. button.classList.add('dragging');
  466. }
  467.  
  468. function dragEnd() {
  469. if (!isDragging) return;
  470. isDragging = false;
  471. button.classList.remove('dragging');
  472. const rect = button.getBoundingClientRect();
  473. const viewportWidth = window.innerWidth;
  474. const threshold = viewportWidth * 0.3;
  475. if (rect.left > viewportWidth - threshold) {
  476. setButtonPosition(viewportWidth - rect.width, rect.top);
  477. button.classList.add('docked');
  478. } else if (rect.left < threshold) {
  479. setButtonPosition(0, rect.top);
  480. button.classList.add('docked');
  481. }
  482. }
  483.  
  484. function drag(e) {
  485. if (!isDragging) return;
  486. e.preventDefault();
  487. if (dragThrottle) return;
  488. dragThrottle = setTimeout(() => {
  489. dragThrottle = null;
  490. }, 16);
  491. const { x: currentX, y: currentY } = getEventPosition(e);
  492. const deltaX = currentX - startX;
  493. const deltaY = currentY - startY;
  494.  
  495. requestAnimationFrame(() => {
  496. setButtonPosition(initialX + deltaX, initialY + deltaY);
  497. lastValidX = initialX + deltaX;
  498. lastValidY = initialY + deltaY;
  499. });
  500. }
  501.  
  502. button.addEventListener('touchstart', dragStart, { passive: false });
  503. button.addEventListener('touchend', dragEnd, { passive: false });
  504. button.addEventListener('touchmove', drag, { passive: false });
  505. button.addEventListener('mousedown', dragStart);
  506. document.addEventListener('mousemove', drag);
  507. document.addEventListener('mouseup', dragEnd);
  508. window.addEventListener('resize', () => {
  509. if (lastValidX !== undefined && lastValidY !== undefined) {
  510. setButtonPosition(lastValidX, lastValidY);
  511. }
  512. });
  513. const savedPosition = GM_getValue('buttonPosition');
  514. if (savedPosition) {
  515. setButtonPosition(savedPosition.x, savedPosition.y);
  516. } else {
  517. setButtonPosition(
  518. window.innerWidth - button.offsetWidth - 20,
  519. window.innerHeight / 2 - button.offsetHeight / 2
  520. );
  521. }
  522. }
  523.  
  524. function createModalContainer(title, contentElement, footerElement) {
  525. const container = createElement('div');
  526. const header = createElement('div', {
  527. className: 'modal-header',
  528. textContent: title
  529. });
  530. container.appendChild(header);
  531. container.appendChild(contentElement);
  532. container.appendChild(footerElement);
  533. return container;
  534. }
  535.  
  536. function createFormGroup(labelText, inputOptions) {
  537. const group = createElement('div', { className: 'form-group' });
  538. const label = createElement('label', {
  539. className: 'form-label',
  540. textContent: labelText
  541. });
  542. let input;
  543. if (inputOptions.type === 'textarea') {
  544. input = createElement('textarea', inputOptions);
  545. } else {
  546. input = createElement('input', inputOptions);
  547. }
  548. group.appendChild(label);
  549. group.appendChild(input);
  550. return { group, input };
  551. }
  552.  
  553. function createPromptModal({ title, promptId, promptContent, isEditable, onSave, modal, overlay }) {
  554. const content = createElement('div', { className: 'modal-content' });
  555. const { group: idGroup, input: idInput } = createFormGroup('Prompt ID', {
  556. className: 'form-input',
  557. type: 'text',
  558. value: promptId || '',
  559. placeholder: '输入Prompt的标识名称'
  560. });
  561. if (promptId && !isEditable) {
  562. idInput.disabled = true;
  563. }
  564. content.appendChild(idGroup);
  565. const { group: contentGroup, input: contentInput } = createFormGroup('Prompt Content', {
  566. className: 'form-textarea',
  567. type: 'textarea',
  568. value: promptContent || '',
  569. placeholder: '输入Prompt内容'
  570. });
  571. content.appendChild(contentGroup);
  572. const footerButtons = [
  573. {
  574. text: 'Cancel',
  575. onClick: (modal, overlay) => {
  576. hideModal(modal, overlay);
  577. }
  578. },
  579. {
  580. text: promptId ? 'Update' : 'Save',
  581. onClick: () => {
  582. const idVal = idInput.value.trim();
  583. const contentVal = contentInput.value.trim();
  584. onSave(idVal, contentVal);
  585. }
  586. }
  587. ];
  588. const footer = createModalFooter(footerButtons, modal, overlay);
  589. const container = createModalContainer(title, content, footer);
  590. openModal(modal, overlay, container);
  591. setTimeout(() => contentInput.focus(), 100);
  592. }
  593.  
  594. function showNewPromptModal(modal, overlay) {
  595. createPromptModal({
  596. title: 'New Prompt',
  597. promptId: '',
  598. promptContent: '',
  599. isEditable: true,
  600. onSave: (id, content) => {
  601. if (id && content) {
  602. try {
  603. saveNewPrompt(id, content);
  604. hideModal(modal, overlay);
  605. } catch (err) {
  606. debug.error('Error saving new prompt:', err);
  607. alert('保存失败,请重试');
  608. }
  609. } else {
  610. alert('请填写所有必填字段');
  611. }
  612. },
  613. modal,
  614. overlay
  615. });
  616. }
  617. function showEditPromptModal(modal, overlay, id, content) {
  618. createPromptModal({
  619. title: 'Prompt Editing',
  620. promptId: id,
  621. promptContent: content,
  622. isEditable: false,
  623. onSave: (id, newContent) => {
  624. if (newContent) {
  625. savedPrompts[id] = newContent;
  626. GM_setValue('prompts', savedPrompts);
  627. hideModal(modal, overlay);
  628. showManagePromptsModal(modal, overlay);
  629. updateDatalist();
  630. } else {
  631. alert('内容不能为空');
  632. }
  633. },
  634. modal,
  635. overlay
  636. });
  637. }
  638.  
  639. function showManagePromptsModal(modal, overlay) {
  640. const container = createElement('div');
  641. const header = createElement('div', {
  642. className: 'modal-header',
  643. textContent: 'Manage Prompts'
  644. });
  645. container.appendChild(header);
  646. const content = createElement('div', {
  647. className: 'modal-content'
  648. });
  649. Object.entries(savedPrompts).forEach(([id, promptContent]) => {
  650. const promptGroup = createElement('div', {
  651. className: 'form-group',
  652. style: {
  653. display: 'flex',
  654. alignItems: 'center',
  655. justifyContent: 'space-between',
  656. padding: '8px',
  657. borderBottom: '1px solid #eee'
  658. }
  659. });
  660. const promptId = createElement('div', {
  661. className: 'form-label',
  662. style: { margin: '0', flex: '1' },
  663. textContent: id
  664. });
  665. const buttonGroup = createElement('div', {
  666. className: 'button-group',
  667. style: { marginLeft: '16px' }
  668. });
  669. const editButton = createElement('button', {
  670. className: 'panel-button',
  671. textContent: 'Edit',
  672. onclick: () => {
  673. showEditPromptModal(modal, overlay, id, promptContent);
  674. }
  675. });
  676. const deleteButton = createElement('button', {
  677. className: 'panel-button',
  678. textContent: 'Delete',
  679. onclick: () => {
  680. if (confirm(`确定要删除 "${id}" 吗?`)) {
  681. delete savedPrompts[id];
  682. GM_setValue('prompts', savedPrompts);
  683. promptGroup.remove();
  684. updateDatalist();
  685. }
  686. }
  687. });
  688. buttonGroup.appendChild(editButton);
  689. buttonGroup.appendChild(deleteButton);
  690. promptGroup.appendChild(promptId);
  691. promptGroup.appendChild(buttonGroup);
  692. content.appendChild(promptGroup);
  693. });
  694. container.appendChild(content);
  695. const footer = createElement('div', {
  696. className: 'modal-footer'
  697. });
  698. const closeButton = createElement('button', {
  699. className: 'panel-button',
  700. textContent: 'Close',
  701. onclick: () => {
  702. hideModal(modal, overlay);
  703. }
  704. });
  705. footer.appendChild(closeButton);
  706. container.appendChild(footer);
  707. openModal(modal, overlay, container);
  708. }
  709.  
  710. function updateDatalist() {
  711. const datalist = document.getElementById('prompt-options');
  712. if (!datalist) return;
  713. datalist.textContent = '';
  714. Object.keys(savedPrompts).forEach(id => {
  715. const option = createElement('option', { value: id });
  716. datalist.appendChild(option);
  717. });
  718. }
  719.  
  720. function setupEventListeners(button, panel, modal, overlay) {
  721. panel.addEventListener('click', (e) => {
  722. e.stopPropagation();
  723. });
  724.  
  725. button.addEventListener('click', (e) => {
  726. if(!e.target.classList.contains('dragging')) {
  727. e.stopPropagation();
  728. debug.log('Button clicked');
  729. panel.classList.toggle('visible');
  730. }
  731. });
  732.  
  733. document.addEventListener('click', () => {
  734. if (panel.classList.contains('visible')) {
  735. debug.log('Clicking outside panel, hiding panel');
  736. panel.classList.remove('visible');
  737. }
  738. });
  739.  
  740. panel.addEventListener('click', (e) => {
  741. const btn = e.target.closest('button[data-action]');
  742. if (!btn) return;
  743. const action = btn.dataset.action;
  744. debug.log('Panel button clicked:', action);
  745. try {
  746. if (action === 'new') {
  747. showNewPromptModal(modalRef, overlayRef);
  748. } else if (action === 'manage') {
  749. showManagePromptsModal(modalRef, overlayRef);
  750. }
  751. } catch (error) {
  752. debug.error('Error handling button click:', error);
  753. }
  754. });
  755.  
  756. const promptInput = panel.querySelector('.prompt-input');
  757.  
  758. promptInput.addEventListener('change', async (e) => {
  759. const selectedValue = e.target.value.trim();
  760. const promptContent = savedPrompts[selectedValue];
  761. if (promptContent) {
  762. debug.log('Setting prompt:', selectedValue);
  763. try {
  764. const site = getCurrentSite();
  765. const success = await setPromptWithRetry(site, promptContent);
  766. if (success) {
  767. debug.log('Set prompt: Done');
  768. } else {
  769. debug.error('Set prompt: Failed');
  770. alert('Failed to set prompt. Please try again.');
  771. }
  772. } catch(err) {
  773. debug.error('Error setting prompt:', err);
  774. alert('Error setting prompt: ' + err.message);
  775. }
  776. }
  777. e.target.value = '';
  778. });
  779.  
  780. overlay.addEventListener('click', () => {
  781. hideModal(modal, overlay);
  782. });
  783.  
  784. document.addEventListener('keydown', (e) => {
  785. if (e.key === 'Escape' && modal.classList.contains('visible')) {
  786. hideModal(modal, overlay);
  787. }
  788. });
  789. }
  790.  
  791. async function initialize() {
  792. debug.log('Initializing...');
  793. try {
  794. const currentSite = getCurrentSite();
  795. if(!currentSite) {
  796. debug.error('Unsupported site');
  797. return;
  798. }
  799. savedPrompts = loadPrompts();
  800. const siteConfig = Object.values(CONFIG.sites).find(s => s.id === currentSite);
  801. if (!siteConfig) {
  802. debug.error('Site configuration not found for current site');
  803. return;
  804. }
  805. const editorSelector = siteConfig.selector;
  806. try {
  807. await waitForElement(editorSelector);
  808. debug.log('Editor element found');
  809. } catch(err) {
  810. debug.error('Editor element not found:', err);
  811. return;
  812. }
  813. initializeUI();
  814. debug.log('Initialization complete');
  815. } catch (error) {
  816. debug.error('Initialization failed:', error);
  817. }
  818. }
  819.  
  820. if (document.readyState === 'loading') {
  821. document.addEventListener('DOMContentLoaded', initialize);
  822. } else {
  823. initialize();
  824. }
  825. })();