AI Chat Template Assistant

Universal template system with upward menu and cross-platform support

  1. // ==UserScript==
  2. // @name AI Chat Template Assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description Universal template system with upward menu and cross-platform support
  6. // @author Dieha
  7. // @license MIT
  8. // @match https://aistudio.google.com/app*
  9. // @match https://chat.qwen.ai/*
  10. // @match https://chat.qwen.com/*
  11. // @match https://chat.deepseek.com/*
  12. // @grant GM_addStyle
  13. // @run-at document-idle
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // ================ CONFIGURATION ================
  20. const GLOBAL_CONFIG = {
  21. buttonText: "Шаблоны",
  22. maxMenuDepth: 5,
  23. animationDuration: 200, // ms
  24. retryInterval: 1500,
  25. debugMode: true // Включите для отладки, особенно на AI Studio
  26. };
  27.  
  28. const TEMPLATE_DATA = [
  29. {
  30. categoryName: "Основные Задачи",
  31. items: [
  32. { label: "Код файлов", text: "Напишите полный код изменяемых файл[ов/а]\n" },
  33. {
  34. label: "Объяснения", // Уровень 0 (относительно категории)
  35. subItems: [
  36. { label: "Объясни (ELI5)", text: "Объясни [ТЕМА] так, как будто мне 5 лет." }, // Уровень 1
  37. { label: "Подробно с примерами", text: "Объясни [ТЕМА] подробно, приведи примеры." },
  38. {
  39. label: "Сложные Аналогии", // Уровень 1
  40. subItems: [ // Уровень 2
  41. { label: "Аналогия для Технаря", text: "Объясни [ТЕМА] через аналогию из IT." },
  42. { label: "Аналогия для Гуманитария", text: "Объясни [ТЕМА] через аналогию из литературы."}
  43. ]
  44. }
  45. ]
  46. },
  47. ]
  48. },
  49. {
  50. categoryName: "Работа с кодом",
  51. items: [
  52. {
  53. label: "Рефакторинг",
  54. text: "Проведи рефакторинг следующего кода:\n```\n[ВСТАВЬТЕ КОД]\n```"
  55. },
  56. {
  57. label: "Поиск ошибок",
  58. text: "Найди и исправь ошибки в коде:\n```\n[ВСТАВЬТЕ КОД]\n```"
  59. }
  60. ]
  61. }
  62. ];
  63.  
  64. const PLATFORM_SETTINGS = {
  65. 'aistudio.google.com': {
  66. // buttonContainer: ".prompt-input-wrapper-container", // Оставляем этот, если prepend работает
  67. buttonContainer: ".prompt-input-wrapper-container > .text-wrapper", // Попробуем так, чтобы кнопка была слева от текстового поля
  68. // buttonContainer: ".prompt-input-wrapper-container > div:nth-child(2)", // Если хотим перед первой кнопкой "add-chunk-menu"
  69. textAreaSelector: 'textarea[aria-label="Type something or pick one from prompt gallery"]', // ОБНОВЛЕНО
  70. insertMethod: "react", // Оставляем react, он должен хорошо работать с Angular (на котором построен AI Studio)
  71. buttonStyle: {
  72. background: "#2D2E30",
  73. border: "1px solid #454545",
  74. hoverBackground: "#3A3C3F",
  75. textColor: "#E3E3E3"
  76. }
  77. },
  78. 'chat.deepseek.com': {
  79. buttonContainer: "div.ec4f5d61",
  80. textAreaSelector: "textarea#chat-input",
  81. insertMethod: "advanced", // ВОЗВРАЩАЕМ advanced
  82. buttonStyle: {
  83. background: "transparent",
  84. border: "1px solid #5E5E5E",
  85. hoverBackground: "#2D2F33",
  86. textColor: "#F8FAFF"
  87. }
  88. },
  89. 'chat.qwen.ai': { // Также для chat.qwen.com, т.к. detectPlatform ищет по .includes()
  90. buttonContainer: ".chat-message-input-container-inner > div.flex.items-center.min-h-\\[56px\\]",
  91. textAreaSelector: "textarea#chat-input",
  92. insertMethod: "standard",
  93. buttonStyle: {
  94. background: "#2B2B2B",
  95. border: "1px solid #4A4A4A",
  96. hoverBackground: "#3A3A3A",
  97. textColor: "#E0E0E0"
  98. }
  99. }
  100. };
  101. // Добавим chat.qwen.com, если его конфигурация идентична chat.qwen.ai
  102. if (!PLATFORM_SETTINGS['chat.qwen.com'] && PLATFORM_SETTINGS['chat.qwen.ai']) {
  103. PLATFORM_SETTINGS['chat.qwen.com'] = { ...PLATFORM_SETTINGS['chat.qwen.ai'] };
  104. }
  105.  
  106.  
  107. // ================ GLOBAL STATE ================
  108. let mainMenu = null;
  109. let templateButton = null;
  110. const activeSubmenus = []; // Массив для хранения активных подменю
  111. let currentPlatform = null;
  112.  
  113. // ================ STYLES ================
  114. GM_addStyle(`
  115. .template-system-container {
  116. position: relative;
  117. display: inline-block;
  118. margin-right: 12px;
  119. vertical-align: middle;
  120. z-index: 99999;
  121. }
  122.  
  123. .template-main-button {
  124. display: flex;
  125. align-items: center;
  126. height: 36px;
  127. padding: 0 16px;
  128. border-radius: 8px;
  129. font-size: 14px;
  130. font-family: system-ui, sans-serif;
  131. cursor: pointer;
  132. transition: all 0.2s ease;
  133. box-sizing: border-box;
  134. user-select: none;
  135. }
  136.  
  137. .template-main-button:hover {
  138. filter: brightness(1.1);
  139. }
  140.  
  141. .template-main-button::after {
  142. content: "▼";
  143. font-size: 0.7em;
  144. margin-left: 8px;
  145. opacity: 0.7;
  146. transition: transform 0.2s ease;
  147. }
  148.  
  149. .template-main-button.active::after {
  150. transform: rotate(180deg);
  151. }
  152.  
  153. .template-menu-wrapper {
  154. position: fixed; /* Позиционирование left/bottom/top будет через JS */
  155. min-width: 280px;
  156. max-height: 70vh;
  157. overflow-y: auto;
  158. /* Изначальный сдвиг для анимации "всплытия" */
  159. transform: translateY(10px);
  160. opacity: 0;
  161. visibility: hidden;
  162. /* Плавный переход для opacity и transform, visibility меняется резко с задержкой */
  163. transition: opacity ${GLOBAL_CONFIG.animationDuration}ms ease, transform ${GLOBAL_CONFIG.animationDuration}ms ease, visibility 0s linear ${GLOBAL_CONFIG.animationDuration}ms;
  164. z-index: 100000;
  165. pointer-events: none;
  166. }
  167.  
  168. .template-menu-wrapper.visible {
  169. opacity: 1;
  170. visibility: visible;
  171. transform: translateY(0); /* Возврат на место */
  172. pointer-events: all;
  173. transition-delay: 0s; /* Убрать задержку для visibility при показе */
  174. }
  175.  
  176. .template-menu-content {
  177. background: #2D2E30;
  178. border-radius: 12px; /* Можно оставить 12px со всех сторон или 12px 12px 0 0 если всегда сверху */
  179. box-shadow: 0 0px 32px rgba(0,0,0,0.3); /* Тень изменена, т.к. меню может быть и снизу */
  180. padding: 12px 0;
  181. /* margin-bottom: 10px; Убрано, т.к. позиционирование точное */
  182. }
  183.  
  184. .menu-category-header {
  185. padding: 10px 20px 8px;
  186. font-size: 12px;
  187. font-weight: 600;
  188. color: #909090;
  189. text-transform: uppercase;
  190. letter-spacing: 0.5px;
  191. border-bottom: 1px solid #404040;
  192. margin: 0 12px 6px;
  193. }
  194.  
  195. .menu-item {
  196. position: relative;
  197. padding: 12px 20px;
  198. color: #E3E3E3;
  199. cursor: pointer;
  200. font-size: 14px;
  201. white-space: nowrap;
  202. transition: background 0.15s ease;
  203. margin: 0 8px;
  204. border-radius: 6px;
  205. }
  206.  
  207. .menu-item:hover {
  208. background: #3A3C3F;
  209. }
  210.  
  211. .menu-item.has-submenu::after {
  212. content: "▶";
  213. position: absolute;
  214. right: 16px;
  215. top: 50%;
  216. transform: translateY(-50%);
  217. font-size: 12px;
  218. color: #A0A0A0;
  219. }
  220.  
  221. .submenu-container {
  222. position: fixed; /* ИЗМЕНЕНО: было absolute, теперь fixed для корректного positionSubmenu */
  223. background: #353638;
  224. min-width: 260px;
  225. border-radius: 8px;
  226. box-shadow: 4px 4px 24px rgba(0,0,0,0.3);
  227. padding: 8px 0;
  228. display: none; /* Будет 'block' при показе */
  229. z-index: 100001;
  230. }
  231.  
  232. ::-webkit-scrollbar {
  233. width: 8px;
  234. }
  235. ::-webkit-scrollbar-track {
  236. background: #252526;
  237. border-radius: 4px;
  238. }
  239. ::-webkit-scrollbar-thumb {
  240. background: #454545;
  241. border-radius: 4px;
  242. }
  243. `);
  244.  
  245. // ================ CORE FUNCTIONS ================
  246. function logDebug(...args) {
  247. if (GLOBAL_CONFIG.debugMode) {
  248. console.log("[ACTA]", ...args);
  249. }
  250. }
  251.  
  252. function logError(...args) {
  253. if (GLOBAL_CONFIG.debugMode) {
  254. console.error("[ACTA]", ...args);
  255. }
  256. }
  257.  
  258. function initializeSystem() {
  259. currentPlatform = detectPlatform();
  260.  
  261. if (!currentPlatform) {
  262. logDebug("Platform not detected. Retrying...");
  263. retryInitialization();
  264. return;
  265. }
  266.  
  267. if (!validatePlatformConfig(currentPlatform)) {
  268. logDebug("Platform config invalid or elements not found. Retrying...", currentPlatform);
  269. retryInitialization();
  270. return;
  271. }
  272.  
  273. if (isAlreadyInitialized()) {
  274. logDebug("System already initialized.");
  275. return;
  276. }
  277.  
  278. try {
  279. createUIElements();
  280. setupEventHandlers();
  281. logDebug("Initialization complete for platform:", window.location.hostname);
  282. } catch (error) {
  283. logError("Error during initialization:", error);
  284. }
  285. }
  286.  
  287. function detectPlatform() {
  288. const hostname = window.location.hostname;
  289. for (const domain of Object.keys(PLATFORM_SETTINGS)) {
  290. if (hostname.includes(domain)) return PLATFORM_SETTINGS[domain];
  291. }
  292. return null;
  293. }
  294.  
  295. function validatePlatformConfig(config) {
  296. if (!config || !config.buttonContainer || !config.textAreaSelector) {
  297. logError("Platform config is missing essential selectors.");
  298. return false;
  299. }
  300. const buttonContainerEl = document.querySelector(config.buttonContainer);
  301. const textAreaEl = document.querySelector(config.textAreaSelector);
  302.  
  303. if (!buttonContainerEl) {
  304. logDebug(`Button container ("${config.buttonContainer}") not found.`);
  305. }
  306. if (!textAreaEl) {
  307. logDebug(`Text area ("${config.textAreaSelector}") not found.`);
  308. }
  309. return !!buttonContainerEl && !!textAreaEl;
  310. }
  311.  
  312. function createUIElements() {
  313. const buttonHost = document.querySelector(currentPlatform.buttonContainer);
  314. if (!buttonHost) {
  315. logError("Button host container not found, cannot create UI elements.");
  316. return; // Добавлена проверка
  317. }
  318.  
  319. const container = document.createElement("div");
  320. container.className = "template-system-container";
  321.  
  322. templateButton = document.createElement("div");
  323. templateButton.className = "template-main-button";
  324. templateButton.textContent = GLOBAL_CONFIG.buttonText;
  325.  
  326. Object.assign(templateButton.style, {
  327. background: currentPlatform.buttonStyle.background,
  328. border: currentPlatform.buttonStyle.border,
  329. color: currentPlatform.buttonStyle.textColor
  330. });
  331. templateButton.addEventListener('mouseenter', () => {
  332. if (currentPlatform.buttonStyle.hoverBackground) {
  333. templateButton.style.setProperty('background', currentPlatform.buttonStyle.hoverBackground, 'important');
  334. }
  335. });
  336. templateButton.addEventListener('mouseleave', () => {
  337. templateButton.style.background = currentPlatform.buttonStyle.background;
  338. });
  339.  
  340.  
  341. container.appendChild(templateButton);
  342. buttonHost.prepend(container);
  343.  
  344. mainMenu = document.createElement("div");
  345. mainMenu.className = "template-menu-wrapper";
  346.  
  347. const menuContent = document.createElement("div");
  348. menuContent.className = "template-menu-content";
  349.  
  350. TEMPLATE_DATA.forEach(category => {
  351. const categoryHeader = document.createElement("div");
  352. categoryHeader.className = "menu-category-header";
  353. categoryHeader.textContent = category.categoryName;
  354. menuContent.appendChild(categoryHeader);
  355.  
  356. category.items.forEach(item => {
  357. menuContent.appendChild(createMenuItem(item, 0)); // Начальная глубина 0
  358. });
  359. });
  360.  
  361. mainMenu.appendChild(menuContent);
  362. document.body.appendChild(mainMenu);
  363. }
  364.  
  365. function createMenuItem(itemData, depth) { // depth - глубина текущего элемента
  366. const itemElement = document.createElement("div");
  367. itemElement.className = "menu-item";
  368. itemElement.textContent = itemData.label;
  369.  
  370. if (itemData.subItems && depth < GLOBAL_CONFIG.maxMenuDepth) {
  371. itemElement.classList.add("has-submenu");
  372. const submenu = createSubMenu(itemData.subItems, depth + 1); // Подменю будет на depth + 1
  373. setupSubmenuBehavior(itemElement, submenu, depth + 1); // Передаем глубину создаваемого подменю
  374. } else if (itemData.text) {
  375. itemElement.dataset.template = itemData.text;
  376. }
  377. return itemElement;
  378. }
  379.  
  380. function createSubMenu(items, depth) {
  381. const submenu = document.createElement("div");
  382. submenu.className = "submenu-container";
  383. submenu.dataset.depth = depth; // Сохраняем глубину для управления
  384.  
  385. items.forEach(item => {
  386. submenu.appendChild(createMenuItem(item, depth)); // Элементы подменю на той же глубине, что и само подменю
  387. });
  388. return submenu;
  389. }
  390.  
  391. function removeSubmenu(submenuElement) {
  392. if (!submenuElement) return;
  393. const index = activeSubmenus.indexOf(submenuElement);
  394. if (index > -1) {
  395. activeSubmenus.splice(index, 1);
  396. }
  397. submenuElement.remove();
  398. }
  399.  
  400. function closeSubmenusFromDepth(targetDepth) {
  401. const toRemove = [];
  402. for (let i = activeSubmenus.length - 1; i >= 0; i--) {
  403. const sub = activeSubmenus[i];
  404. if (parseInt(sub.dataset.depth, 10) >= targetDepth) {
  405. toRemove.push(sub); // Собираем для удаления
  406. }
  407. }
  408. toRemove.forEach(sub => removeSubmenu(sub));
  409. }
  410.  
  411. function setupSubmenuBehavior(parentItem, submenu, submenuDepth) {
  412. parentItem.addEventListener("mouseenter", () => {
  413. closeSubmenusFromDepth(submenuDepth); // Закрыть подменю этого и более глубоких уровней от других веток
  414.  
  415. document.body.appendChild(submenu); // Важно добавить в DOM до getBoundingClientRect/offsetWidth
  416. positionSubmenu(parentItem, submenu);
  417. activeSubmenus.push(submenu);
  418. });
  419.  
  420. // Чтобы подменю не закрывалось при переходе с родителя на него
  421. let PItimer, SUtimer;
  422. parentItem.addEventListener("mouseleave", (e) => {
  423. PItimer = setTimeout(() => {
  424. if (!submenu.matches(':hover')) { // Если курсор не над подменю
  425. removeSubmenu(submenu);
  426. }
  427. }, 100); // Небольшая задержка
  428. });
  429. submenu.addEventListener("mouseenter", () => clearTimeout(PItimer)); // Отменить закрытие, если вошли в подменю
  430.  
  431. submenu.addEventListener("mouseleave", (e) => {
  432. SUtimer = setTimeout(() => {
  433. if (!parentItem.matches(':hover')) { // Если курсор не над родительским элементом
  434. removeSubmenu(submenu);
  435. }
  436. }, 100);
  437. });
  438. parentItem.addEventListener("mouseenter", () => clearTimeout(SUtimer)); // Отменить закрытие, если вернулись на родителя
  439. }
  440.  
  441.  
  442. function positionSubmenu(parentElement, submenu) {
  443. submenu.style.display = "block"; // Показать для измерения размеров
  444. const parentRect = parentElement.getBoundingClientRect();
  445. const submenuRect = submenu.getBoundingClientRect(); // Получить размеры после display: block
  446. const viewportWidth = window.innerWidth;
  447. const viewportHeight = window.innerHeight;
  448.  
  449. let left = parentRect.right + 5;
  450. let top = parentRect.top;
  451.  
  452. // Проверка правой границы
  453. if (left + submenuRect.width > viewportWidth - 10) {
  454. left = parentRect.left - submenuRect.width - 5;
  455. }
  456. // Если и слева не помещается (очень широкое подменю или родитель у края)
  457. if (left < 10) {
  458. left = 10;
  459. }
  460.  
  461. // Проверка нижней границы
  462. if (top + submenuRect.height > viewportHeight - 10) {
  463. top = viewportHeight - submenuRect.height - 10;
  464. }
  465. // Если и сверху не помещается (очень высокое подменю или родитель у края)
  466. if (top < 10) {
  467. top = 10;
  468. }
  469.  
  470. submenu.style.left = `${left}px`;
  471. submenu.style.top = `${top}px`;
  472. }
  473.  
  474. function setupEventHandlers() {
  475. if (!templateButton || !mainMenu) {
  476. logError("Cannot setup event handlers: button or menu not created.");
  477. return;
  478. }
  479. templateButton.addEventListener("click", (e) => {
  480. e.stopPropagation();
  481. toggleMainMenu();
  482. });
  483.  
  484. document.addEventListener("click", (e) => {
  485. // Проверяем, был ли клик вне кнопки И вне главного меню И вне любого активного подменю
  486. if (mainMenu.classList.contains("visible") &&
  487. !templateButton.contains(e.target) &&
  488. !mainMenu.contains(e.target) &&
  489. !activeSubmenus.some(submenu => submenu.contains(e.target))) {
  490. closeAllMenus();
  491. }
  492. });
  493.  
  494. // Обработчик выбора шаблона (привязан к mainMenu, чтобы не слушать весь документ без нужды)
  495. mainMenu.addEventListener("click", (e) => {
  496. const targetItem = e.target.closest(".menu-item:not(.has-submenu)"); // Только элементы без подменю
  497. if (targetItem && targetItem.dataset.template) {
  498. insertTemplateText(targetItem.dataset.template);
  499. closeAllMenus();
  500. }
  501. });
  502. // То же для подменю (они в body, поэтому слушаем body, но проверяем класс)
  503. document.body.addEventListener("click", (e) => {
  504. const targetItem = e.target.closest(".submenu-container .menu-item:not(.has-submenu)");
  505. if (targetItem && targetItem.dataset.template) {
  506. insertTemplateText(targetItem.dataset.template);
  507. closeAllMenus();
  508. }
  509. });
  510.  
  511.  
  512. window.addEventListener("resize", () => { if(mainMenu.classList.contains("visible")) updateMenuPosition(); });
  513. window.addEventListener("scroll", () => { if(mainMenu.classList.contains("visible")) updateMenuPosition(); }, true);
  514. }
  515.  
  516. function toggleMainMenu() {
  517. const становитсяВидимым = !mainMenu.classList.contains("visible");
  518. mainMenu.classList.toggle("visible");
  519. templateButton.classList.toggle("active");
  520.  
  521. if (становитсяВидимым) {
  522. updateMenuPosition(); // Позиционируем при открытии
  523. } else {
  524. closeSubmenusFromDepth(0); // Закрыть все подменю при закрытии главного
  525. }
  526. }
  527.  
  528. function updateMenuPosition() {
  529. if (!mainMenu || !mainMenu.classList.contains("visible") || !templateButton) return;
  530.  
  531. const buttonRect = templateButton.getBoundingClientRect();
  532. mainMenu.style.visibility = 'hidden'; // Скрыть на время измерений, чтобы избежать мигания
  533. mainMenu.style.display = 'block'; // Убедиться, что display не none
  534. const menuHeight = mainMenu.offsetHeight;
  535. const menuWidth = mainMenu.offsetWidth;
  536. mainMenu.style.display = ''; // Вернуть как было (если был не block)
  537. mainMenu.style.visibility = ''; // Вернуть видимость
  538.  
  539. const viewportHeight = window.innerHeight;
  540. const viewportWidth = window.innerWidth;
  541. const margin = 10; // Отступ от кнопки
  542.  
  543. // Вертикальное позиционирование
  544. // Приоритет НАД кнопкой, если кнопка в нижней половине И есть место,
  545. // ИЛИ если места сверху больше, чем снизу (и снизу не хватает)
  546. let menuTop, menuBottom = "auto";
  547.  
  548. if ( (buttonRect.top > viewportHeight / 2 && buttonRect.top >= menuHeight + margin) ||
  549. (buttonRect.top >= menuHeight + margin && (viewportHeight - buttonRect.bottom) < menuHeight + margin && (viewportHeight - buttonRect.bottom) < buttonRect.top) ) {
  550. // Позиционируем НАД кнопкой
  551. menuTop = "auto";
  552. menuBottom = `${viewportHeight - buttonRect.top + margin}px`;
  553. mainMenu.style.borderRadius = "12px 12px 0 0"; // Если меню сверху
  554. } else {
  555. // Позиционируем ПОД кнопкой
  556. menuTop = `${buttonRect.bottom + margin}px`;
  557. menuBottom = "auto";
  558. mainMenu.style.borderRadius = "0 0 12px 12px"; // Если меню снизу
  559. }
  560. // Коррекция, если меню выходит за пределы экрана по высоте
  561. if (menuTop !== "auto") {
  562. const topNum = parseFloat(menuTop);
  563. if (topNum < margin) menuTop = `${margin}px`;
  564. if (topNum + menuHeight > viewportHeight - margin) {
  565. menuTop = `${Math.max(margin, viewportHeight - menuHeight - margin)}px`;
  566. }
  567. } else if (menuBottom !== "auto") {
  568. const bottomNum = parseFloat(menuBottom);
  569. // Если mainMenu.style.bottom установлено, то top вычисляется браузером.
  570. // Нам нужно проверить, не уходит ли верхний край меню за пределы viewport.
  571. // (viewportHeight - bottomNum - menuHeight) - это будет координата top.
  572. if ((viewportHeight - bottomNum - menuHeight) < margin) {
  573. menuBottom = `${Math.max(margin, viewportHeight - menuHeight - margin)}px`;
  574. // Это не совсем то, это изменит bottom, что сдвинет top.
  575. // Лучше установить maxHeight, если не помещается
  576. }
  577. }
  578.  
  579. mainMenu.style.top = menuTop;
  580. mainMenu.style.bottom = menuBottom;
  581.  
  582.  
  583. // Горизонтальное позиционирование
  584. if (buttonRect.left + menuWidth > viewportWidth - 20) {
  585. mainMenu.style.left = "auto";
  586. mainMenu.style.right = `${Math.max(10, viewportWidth - (buttonRect.right))}px`; // Выровнять по правому краю кнопки или по краю экрана
  587. } else {
  588. mainMenu.style.left = `${buttonRect.left}px`;
  589. mainMenu.style.right = "auto";
  590. }
  591. }
  592.  
  593. function insertTemplateText(text) {
  594. const textArea = document.querySelector(currentPlatform.textAreaSelector);
  595. if (!textArea) {
  596. logError("Textarea not found for inserting text.");
  597. return;
  598. }
  599.  
  600. // ИЗМЕНЕНИЕ ЗДЕСЬ: Убираем обработку плейсхолдеров с prompt
  601. let finalTtext = text;
  602. // Конец изменения
  603.  
  604. try {
  605. // Запоминаем текущее значение и позицию курсора
  606. const start = textArea.selectionStart;
  607. const end = textArea.selectionEnd;
  608. const originalValue = textArea.value;
  609.  
  610. // Формируем новый текст
  611. const newValue = originalValue.substring(0, start) + finalTtext + originalValue.substring(end);
  612.  
  613. if (currentPlatform.insertMethod === "react") {
  614. insertForReact(textArea, newValue, finalTtext.length, start);
  615. } else if (currentPlatform.insertMethod === "advanced") {
  616. insertWithEvents(textArea, newValue, finalTtext.length, start);
  617. } else {
  618. standardInsert(textArea, newValue, finalTtext.length, start);
  619. }
  620. } catch (error) {
  621. logError("Insert error, trying fallback:", error);
  622. fallbackInsert(textArea, finalTtext); // В fallback передаем только сам шаблон
  623. }
  624. textArea.focus();
  625. }
  626.  
  627. // Обновленные функции вставки для установки курсора после вставленного текста
  628. function setCursorPosition(textArea, position) {
  629. textArea.selectionStart = textArea.selectionEnd = position;
  630. }
  631.  
  632. function insertForReact(textArea, text, insertedTextLength, originalStart) {
  633. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
  634. nativeInputValueSetter.call(textArea, text);
  635. const ev = new Event("input", { bubbles: true });
  636. textArea.dispatchEvent(ev);
  637. setCursorPosition(textArea, originalStart + insertedTextLength);
  638. }
  639.  
  640. function insertWithEvents(textArea, text, insertedTextLength, originalStart) {
  641. // Попробуем сначала "родной" способ, как в React, на случай если он сработает
  642. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
  643. if (nativeInputValueSetter) {
  644. nativeInputValueSetter.call(textArea, text);
  645. } else {
  646. textArea.value = text; // Если сеттер не нашелся, используем прямое присваивание
  647. }
  648.  
  649. // Отправляем разнообразные события
  650. // Порядок может иметь значение
  651. textArea.dispatchEvent(new Event('focus', { bubbles: true, cancelable: true }));
  652. textArea.dispatchEvent(new Event('keydown', { bubbles: true, cancelable: true, key: 'a', char: 'a', keyCode: 65 })); // имитация нажатия клавиши
  653. textArea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true, inputType: 'insertText' }));
  654. textArea.dispatchEvent(new Event('keyup', { bubbles: true, cancelable: true, key: 'a', char: 'a', keyCode: 65 }));
  655. textArea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  656. // textArea.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true })); // Может быть не нужно сразу блюрить
  657.  
  658. setCursorPosition(textArea, originalStart + insertedTextLength);
  659. // Иногда нужно еще раз сфокусироваться после всех манипуляций
  660. textArea.focus();
  661. }
  662.  
  663. function standardInsert(textArea, text, insertedTextLength, originalStart) {
  664. textArea.value = text;
  665. textArea.dispatchEvent(new Event("input", { bubbles: true }));
  666. setCursorPosition(textArea, originalStart + insertedTextLength);
  667. }
  668.  
  669. function fallbackInsert(textArea, textToInsert) {
  670. textArea.focus();
  671. // document.execCommand более не рекомендуется, но может быть последним средством
  672. // Простая вставка в текущую позицию курсора
  673. const start = textArea.selectionStart;
  674. const end = textArea.selectionEnd;
  675. textArea.value = textArea.value.substring(0, start) + textToInsert + textArea.value.substring(end);
  676. setCursorPosition(textArea, start + textToInsert.length);
  677. }
  678.  
  679.  
  680. function closeAllMenus() {
  681. if (mainMenu) mainMenu.classList.remove("visible");
  682. if (templateButton) templateButton.classList.remove("active");
  683. closeSubmenusFromDepth(0); // Закрыть все подменю (глубина 0 и больше)
  684. }
  685.  
  686. function retryInitialization() {
  687. logDebug(`Retrying initialization in ${GLOBAL_CONFIG.retryInterval}ms...`);
  688. setTimeout(initializeSystem, GLOBAL_CONFIG.retryInterval);
  689. }
  690.  
  691. function isAlreadyInitialized() {
  692. return !!document.querySelector(".template-system-container");
  693. }
  694.  
  695. // ================ INITIALIZATION ================
  696. // Запускаем инициализацию после полной загрузки страницы или с небольшой задержкой,
  697. // чтобы дать шанс динамическим элементам появиться.
  698. if (document.readyState === "complete" || document.readyState === "interactive") {
  699. setTimeout(initializeSystem, 500); // Небольшая задержка перед первой попыткой
  700. } else {
  701. window.addEventListener("DOMContentLoaded", () => setTimeout(initializeSystem, 500));
  702. }
  703.  
  704. // MutationObserver для отслеживания изменений в DOM, если начальная инициализация не удалась
  705. const observer = new MutationObserver((mutations, obs) => {
  706. if (!isAlreadyInitialized()) {
  707. logDebug("DOM changed, attempting re-initialization.");
  708. initializeSystem(); // Попытка инициализации при изменениях DOM
  709. }
  710. // Если уже инициализировано, можно остановить наблюдение, если это целесообразно
  711. // else { obs.disconnect(); logDebug("System initialized, observer disconnected."); }
  712. });
  713.  
  714. // Начинаем наблюдение за body, если элементы могут появляться динамически
  715. observer.observe(document.body, {
  716. childList: true,
  717. subtree: true
  718. });
  719.  
  720. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址