Size
92.0 KB
Version
1.3.59
Created
Mar 19, 2026
Updated
23 days ago
1// ==UserScript==
2// @name Ozon AI Answer Generator
3// @description AI-powered answer generator for Ozon seller questions
4// @version 1.3.59
5// @match https://*.seller.ozon.ru/*
6// @icon https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.xmlhttpRequest
10// @grant none
11// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
12// ==/UserScript==
13(function() {
14 'use strict';
15
16 TM_log('Ozon AI Answer Generator initialized');
17
18 // Debounce функция для оптимизации
19 function debounce(func, wait) {
20 let timeout;
21 return function executedFunction(...args) {
22 const later = () => {
23 clearTimeout(timeout);
24 func(...args);
25 };
26 clearTimeout(timeout);
27 timeout = setTimeout(later, wait);
28 };
29 }
30
31 // Функция для добавления стилей
32 function addStyles() {
33 const styles = `
34 .ai-generator-container {
35 margin: 16px 0;
36 padding: 16px;
37 background: #f5f5f7;
38 border-radius: 8px;
39 border: 1px solid #e0e0e0;
40 }
41
42 .ai-generator-title {
43 font-size: 14px;
44 font-weight: 600;
45 margin-bottom: 12px;
46 color: #1a1a1a;
47 }
48
49 .ai-prompt-input {
50 width: 100%;
51 padding: 10px 12px;
52 border: 1px solid #d1d1d6;
53 border-radius: 6px;
54 font-size: 14px;
55 font-family: inherit;
56 resize: vertical;
57 min-height: 60px;
58 margin-bottom: 12px;
59 box-sizing: border-box;
60 }
61
62 .ai-prompt-input:focus {
63 outline: none;
64 border-color: #005bff;
65 box-shadow: 0 0 0 3px rgba(0, 91, 255, 0.1);
66 }
67
68 .ai-generate-btn {
69 background: #005bff;
70 color: white;
71 border: none;
72 padding: 10px 20px;
73 border-radius: 6px;
74 font-size: 14px;
75 font-weight: 500;
76 cursor: pointer;
77 transition: background 0.2s;
78 width: 100%;
79 }
80
81 .ai-generate-btn:hover {
82 background: #0047cc;
83 }
84
85 .ai-generate-btn:disabled {
86 background: #d1d1d6;
87 cursor: not-allowed;
88 }
89
90 .ai-generate-btn.loading {
91 position: relative;
92 color: transparent;
93 }
94
95 .ai-generate-btn.loading::after {
96 content: '';
97 position: absolute;
98 width: 16px;
99 height: 16px;
100 top: 50%;
101 left: 50%;
102 margin-left: -8px;
103 margin-top: -8px;
104 border: 2px solid #ffffff;
105 border-radius: 50%;
106 border-top-color: transparent;
107 animation: spinner 0.6s linear infinite;
108 }
109
110 @keyframes spinner {
111 to { transform: rotate(360deg); }
112 }
113
114 .ai-prompt-hint {
115 font-size: 12px;
116 color: #666;
117 margin-bottom: 8px;
118 }
119 .answer-textarea-container {
120 margin-top: 12px;
121 padding: 12px;
122 background: #f8f9fa;
123 border-radius: 6px;
124 border: 1px solid #dee2e6;
125 }
126
127 .answer-textarea {
128 width: 100%;
129 min-height: 150px;
130 padding: 8px;
131 border: 1px solid #ced4da;
132 border-radius: 4px;
133 font-size: 13px;
134 font-family: inherit;
135 resize: vertical;
136 box-sizing: border-box;
137 }
138
139 .answer-textarea:focus {
140 outline: none;
141 border-color: #005bff;
142 box-shadow: 0 0 0 2px rgba(0, 91, 255, 0.1);
143 }
144 `;
145
146 TM_addStyle(styles);
147 }
148
149 // Функция для создания UI генератора
150 function createGeneratorUI(modal) {
151 // Проверяем, не создан ли уже UI
152 if (modal.querySelector('.ai-generator-container')) {
153 TM_log('AI Generator UI already exists');
154 return;
155 }
156
157 // Находим заголовок "Ответ на вопрос"
158 const answerTitleContainer = modal.querySelector('.mt7');
159 if (!answerTitleContainer) {
160 TM_log('ERROR: Answer title container not found in modal');
161 return;
162 }
163
164 // Находим textarea для ответа (для проверки)
165 const textarea = modal.querySelector('textarea');
166 if (!textarea) {
167 TM_log('ERROR: Textarea not found in modal');
168 return;
169 }
170
171 // Создаем контейнер для AI генератора
172 const container = document.createElement('div');
173 container.className = 'ai-generator-container';
174
175 container.innerHTML = `
176 <div class="ai-generator-title">🤖 AI Генератор ответов</div>
177 <div class="ai-prompt-hint">Дополнительные инструкции для AI (необязательно):</div>
178 <textarea class="ai-prompt-input" placeholder="Например: Ответь кратко и дружелюбно, упомяни возрастные ограничения..."></textarea>
179 <button class="ai-generate-btn" data-generated="false">Сгенерировать ответ</button>
180 `;
181
182 // Вставляем контейнер после заголовка "Ответ на вопрос"
183 answerTitleContainer.insertAdjacentElement('afterend', container);
184
185 // Добавляем обработчик на кнопку
186 const generateBtn = container.querySelector('.ai-generate-btn');
187 const promptInput = container.querySelector('.ai-prompt-input');
188
189 generateBtn.addEventListener('click', async () => {
190 await generateAnswer(modal, generateBtn, promptInput, textarea);
191 });
192
193 TM_log('AI Generator UI created successfully');
194 }
195
196 // Функция для генерации ответа через AI
197 async function generateAnswer(modal, button, promptInput, textarea) {
198 try {
199 // Отключаем кнопку и показываем загрузку
200 button.disabled = true;
201 button.classList.add('loading');
202
203 // Получаем информацию о продукте
204 const productNameElement = modal.querySelector('.mb1');
205 const productName = productNameElement ? productNameElement.textContent.trim() : 'Неизвестный продукт';
206
207 // Получаем артикул продавца из модального окна - ищем по тексту "Артикул"
208 const articleLabel = Array.from(modal.querySelectorAll('*'))
209 .find(el => el.textContent.trim() === 'Артикул' && el.children.length === 0);
210 const articleContainer = articleLabel ? articleLabel.parentElement : null;
211
212 // Ищем следующий элемент с текстом (это будет значение артикула)
213 let sellerArticle = '';
214 if (articleContainer) {
215 const allDivs = Array.from(articleContainer.querySelectorAll('div'));
216 const articleValueDiv = allDivs.find(div =>
217 div.textContent.trim() !== 'Артикул' &&
218 div.textContent.trim().length > 0 &&
219 div.children.length === 0
220 );
221 sellerArticle = articleValueDiv ? articleValueDiv.textContent.trim() : '';
222 }
223
224 TM_log('=== ГЕНЕРАЦИЯ ОТВЕТА ===');
225 TM_log('Артикул продавца:', sellerArticle);
226
227 // Получаем текст вопроса - ищем по тексту "Вопрос"
228 const questionLabel = Array.from(modal.querySelectorAll('*'))
229 .find(el => el.textContent.trim() === 'Вопрос' && el.children.length === 0);
230
231 if (!questionLabel) {
232 TM_log('ERROR: Question label not found');
233 throw new Error('Не удалось найти метку "Вопрос"');
234 }
235
236 // Находим родительский контейнер
237 const questionContainer = questionLabel.parentElement;
238 if (!questionContainer) {
239 TM_log('ERROR: Question container not found');
240 throw new Error('Не удалось найти контейнер вопроса');
241 }
242
243 // Ищем текст вопроса - это следующий div с текстом
244 const allDivs = Array.from(questionContainer.querySelectorAll('div'));
245 const questionTextDiv = allDivs.find(div =>
246 div.textContent.trim() !== 'Вопрос' &&
247 div.textContent.trim().length > 10 &&
248 div.children.length === 0
249 );
250
251 const questionText = questionTextDiv ? questionTextDiv.textContent.trim() : '';
252
253 if (!questionText) {
254 TM_log('ERROR: Question text not found');
255 TM_log('Question container HTML:', questionContainer.innerHTML);
256 throw new Error('Не удалось найти текст вопроса');
257 }
258
259 TM_log('Название продукта:', productName);
260 TM_log('Текст вопроса:', questionText);
261
262 // Получаем дополнительный промпт
263 const additionalPrompt = promptInput.value.trim();
264 if (additionalPrompt) {
265 TM_log('Дополнительные инструкции:', additionalPrompt);
266 }
267
268 // Ищем товар в базе знаний по артикулу продавца
269 const productInfo = findProductInKnowledgeBase(sellerArticle);
270 let knowledgeBaseInfo = '';
271
272 if (productInfo) {
273 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
274 TM_log('✅ ТОВАР НАЙДЕН В БАЗЕ ЗНАНИЙ');
275 TM_log('Информация из базы знаний:', knowledgeBaseInfo);
276 } else {
277 TM_log('❌ ТОВАР НЕ НАЙДЕН В БАЗЕ ЗНАНИЙ');
278 TM_log('Артикул для поиска:', sellerArticle);
279 }
280
281 // Используем кастомный промпт или дефолтный
282 const promptTemplate = getCurrentPrompt();
283
284 // Заменяем переменные в промпте
285 let mainPrompt = promptTemplate
286 .replace('{productName}', productName)
287 .replace('{questionText}', questionText)
288 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo)
289 .replace('{additionalPrompt}', additionalPrompt);
290
291 TM_log('=== ПРОМПТ ДЛЯ AI ===');
292 TM_log(mainPrompt);
293 TM_log('=== КОНЕЦ ПРОМПТА ===');
294
295 // Вызываем AI
296 const answer = await callAI(mainPrompt);
297
298 TM_log('=== ОТВЕТ ОТ AI ===');
299 TM_log(answer);
300 TM_log('=== КОНЕЦ ОТВЕТА ===');
301
302 // Проверяем валидность ответа
303 if (!isValidAnswer(answer)) {
304 TM_log('⚠️ AI вернул невалидный ответ или SKIP');
305 alert('AI не смог сгенерировать ответ: недостаточно информации о товаре');
306 return;
307 }
308
309 // Вставляем ответ в textarea
310 textarea.value = answer;
311
312 // Генерируем событие input для обновления состояния формы
313 const inputEvent = new Event('input', { bubbles: true });
314 textarea.dispatchEvent(inputEvent);
315
316 // Меняем текст кнопки на "Перегенерировать"
317 button.textContent = 'Перегенерировать ответ';
318 button.setAttribute('data-generated', 'true');
319
320 TM_log('✅ Ответ успешно сгенерирован и вставлен');
321
322 } catch (error) {
323 TM_log('❌ Ошибка при генерации ответа:', error.message);
324 alert('Ошибка при генерации ответа: ' + error.message);
325 } finally {
326 // Включаем кнопку обратно
327 button.disabled = false;
328 button.classList.remove('loading');
329 }
330 }
331
332 // Функция для проверки, является ли ответ валидным
333 function isValidAnswer(answer) {
334 if (!answer || typeof answer !== 'string') {
335 return false;
336 }
337
338 const trimmedAnswer = answer.trim().toUpperCase();
339
340 // Проверяем на SKIP
341 if (trimmedAnswer === 'SKIP') {
342 return false;
343 }
344
345 // Проверяем на слишком короткий ответ (меньше 10 символов)
346 if (answer.trim().length < 10) {
347 return false;
348 }
349
350 // Проверяем на типичные фразы отказа
351 const refusalPhrases = [
352 'не могу ответить',
353 'недостаточно информации',
354 'не знаю',
355 'cannot answer',
356 'don\'t know',
357 'insufficient information',
358 'нет информации'
359 ];
360
361 const lowerAnswer = answer.toLowerCase();
362 for (const phrase of refusalPhrases) {
363 if (lowerAnswer.includes(phrase)) {
364 return false;
365 }
366 }
367
368 return true;
369 }
370
371 // Наблюдатель за появлением модального окна
372 function observeModal() {
373 const observer = new MutationObserver(debounce(() => {
374 // Ищем модальное окно по тексту заголовка "Вопрос о товаре"
375 const allHeadings = document.querySelectorAll('*');
376 for (const element of allHeadings) {
377 // Проверяем, что это заголовок с текстом "Вопрос о товаре"
378 if (element.textContent.trim() === 'Вопрос о товаре' &&
379 element.classList.contains('heading-500')) {
380 // Ищем родительское модальное окно
381 let modal = element;
382 while (modal && modal.tagName !== 'BODY') {
383 // Модальное окно обычно имеет aria-label или role
384 if (modal.hasAttribute('aria-label') || modal.getAttribute('role') === 'dialog') {
385 TM_log('Question modal detected by content');
386 createGeneratorUI(modal);
387 return;
388 }
389 modal = modal.parentElement;
390 }
391 }
392 }
393 }, 300));
394
395 observer.observe(document.body, {
396 childList: true,
397 subtree: true
398 });
399
400 TM_log('Modal observer started');
401 }
402
403 // Инициализация
404 function init() {
405 TM_log('Starting Ozon AI Answer Generator...');
406
407 // Добавляем стили
408 addStyles();
409
410 // Проверяем, открыто ли уже модальное окно
411 const allHeadings = document.querySelectorAll('*');
412 for (const element of allHeadings) {
413 if (element.textContent.trim() === 'Вопрос о товаре' &&
414 element.classList.contains('heading-500')) {
415 let modal = element;
416 while (modal && modal.tagName !== 'BODY') {
417 if (modal.hasAttribute('aria-label') || modal.getAttribute('role') === 'dialog') {
418 TM_log('Existing question modal found');
419 createGeneratorUI(modal);
420 break;
421 }
422 modal = modal.parentElement;
423 }
424 break;
425 }
426 }
427
428 // Запускаем наблюдатель
429 observeModal();
430 }
431
432 // Запускаем после загрузки DOM
433 if (document.readyState === 'loading') {
434 document.addEventListener('DOMContentLoaded', init);
435 } else {
436 init();
437 }
438
439 // ============= МАССОВАЯ ГЕНЕРАЦИЯ ОТВЕТОВ =============
440
441 // Хранилище для сгенерированных ответов
442 let generatedAnswers = new Map();
443
444 // Хранилище базы знаний
445 let knowledgeBase = null;
446
447 // Хранилище промпта
448 let customPrompt = null;
449
450 // Хранилище настроек модели
451 let modelSettings = {
452 provider: 'rmcall', // 'rmcall' или 'openrouter'
453 model: 'google/gemini-2.0-flash-exp:free',
454 apiKey: '',
455 customModels: [] // Список кастомных моделей
456 };
457
458 // Дефолтный промпт
459 const DEFAULT_PROMPT = 'Ты - профессиональный менеджер по работе с клиентами на маркетплейсе Ozon.\n\n' +
460 'Продукт: {productName}\n\n' +
461 'Вопрос покупателя: {questionText}{knowledgeBaseInfo}\n\n' +
462 'Задача: Сгенерируй профессиональный, вежливый и информативный ответ на вопрос покупателя о товаре.\n\n' +
463 'Требования к ответу:\n' +
464 '- Будь вежливым и дружелюбным\n' +
465 '- Отвечай по существу вопроса\n' +
466 '- Используй информацию о продукте\n' +
467 '- Ответ должен быть кратким (2-4 предложения)\n' +
468 '- Не придумывай характеристики, которых нет в названии продукта\n' +
469 '- Если нужна дополнительная информация, вежливо предложи обратиться к описанию товара\n' +
470 '- Учитывай при ответе Дополнительные инструкции {additionalPrompt} если они есть\n' +
471 '- ВАЖНО: Если у тебя недостаточно информации для ответа или ты не знаешь что ответить, напиши ТОЛЬКО слово "SKIP" без дополнительных объяснений';
472
473 // Функция для загрузки базы знаний из localStorage
474 async function loadKnowledgeBase() {
475 try {
476 const stored = localStorage.getItem('ozon_knowledge_base');
477 if (stored) {
478 knowledgeBase = JSON.parse(stored);
479 TM_log('Knowledge base loaded:', knowledgeBase.length, 'products');
480 updateKnowledgeBaseStatus();
481 }
482 } catch (error) {
483 console.error('Error loading knowledge base:', error);
484 }
485 }
486
487 // Функция для сохранения базы знаний в localStorage
488 function saveKnowledgeBase(data) {
489 try {
490 localStorage.setItem('ozon_knowledge_base', JSON.stringify(data));
491 knowledgeBase = data;
492 TM_log('Knowledge base saved:', data.length, 'products');
493 updateKnowledgeBaseStatus();
494 } catch (error) {
495 console.error('Error saving knowledge base:', error);
496 alert('Ошибка при сохранении базы знаний: ' + error.message);
497 }
498 }
499
500 // Функция для обновления статуса базы знаний
501 function updateKnowledgeBaseStatus() {
502 const statusDiv = document.querySelector('.kb-status');
503 if (statusDiv) {
504 if (knowledgeBase && knowledgeBase.length > 0) {
505 statusDiv.textContent = `База знаний загружена: ${knowledgeBase.length} товаров`;
506 statusDiv.style.color = '#28a745';
507 } else {
508 statusDiv.textContent = 'База знаний не загружена';
509 statusDiv.style.color = '#666';
510 }
511 }
512 }
513
514 // Функция для поиска товара в базе знаний по SKU
515 function findProductInKnowledgeBase(sellerArticle) {
516 if (!knowledgeBase || knowledgeBase.length === 0) {
517 TM_log('Knowledge base is empty');
518 return null;
519 }
520
521 if (!sellerArticle) {
522 TM_log('Seller article is empty');
523 return null;
524 }
525
526 TM_log('Searching for seller article:', sellerArticle);
527
528 // Ищем товар по артикулу - проверяем несколько возможных названий колонок
529 const possibleColumns = [
530 'Штрихкод (Серийный номер / EAN)',
531 'SKU',
532 'Артикул',
533 'Штрихкод',
534 'Серийный номер',
535 'EAN',
536 'Артикул продавца'
537 ];
538
539 let foundProduct = null;
540 let foundColumn = '';
541
542 for (const item of knowledgeBase) {
543 for (const column of possibleColumns) {
544 const itemSku = item[column];
545 if (itemSku && itemSku.toString().trim() === sellerArticle) {
546 foundProduct = item;
547 foundColumn = column;
548 break;
549 }
550 }
551 if (foundProduct) break;
552 }
553
554 if (foundProduct) {
555 TM_log(`Product found in knowledge base using column "${foundColumn}"`);
556 TM_log('Product data:', foundProduct);
557 } else {
558 TM_log('Product not found in knowledge base for article:', sellerArticle);
559 TM_log('Available columns in first product:', knowledgeBase[0] ? Object.keys(knowledgeBase[0]) : 'No products');
560 }
561
562 return foundProduct;
563 }
564
565 // Функция для форматирования информации о товаре из базы знаний
566 function formatProductInfo(product) {
567 const info = [];
568
569 if (product['Название товара']) info.push(`Название: ${product['Название товара']}`);
570 if (product['Бренд*']) info.push(`Бренд: ${product['Бренд*']}`);
571 if (product['Тип*']) info.push(`Тип: ${product['Тип*']}`);
572 if (product['Состав*']) info.push(`Состав: ${product['Состав*']}`);
573 if (product['Аннотация']) info.push(`Описание: ${product['Аннотация']}`);
574 if (product['Целевая аудитория']) info.push(`Целевая аудитория: ${product['Целевая аудитория']}`);
575 if (product['Направление БАД']) info.push(`Направление: ${product['Направление БАД']}`);
576 if (product['Вкусовой акцент (вкус)']) info.push(`Вкус: ${product['Вкусовой акцент (вкус)']}`);
577 if (product['Форма выпуска продукта']) info.push(`Форма выпуска: ${product['Форма выпуска продукта']}`);
578 if (product['Способ применения']) info.push(`Способ применения: ${product['Способ применения']}`);
579 if (product['Срок годности в днях']) info.push(`Срок годности: ${product['Срок годности в днях']} дней`);
580 if (product['Страна-изготовитель']) info.push(`Страна-изготовитель: ${product['Страна-изготовитель']}`);
581 if (product['Для детей']) info.push(`Для детей: ${product['Для детей']}`);
582 if (product['Минимальный возраст от']) info.push(`Минимальный возраст: ${product['Минимальный возраст от']}`);
583
584 return info.join('\n');
585 }
586
587 // Функция для обработки загрузки XLS файла
588 function handleFileUpload(event) {
589 const file = event.target.files[0];
590 if (!file) return;
591
592 const reader = new FileReader();
593
594 reader.onload = function(e) {
595 try {
596 const data = new Uint8Array(e.target.result);
597 const workbook = XLSX.read(data, { type: 'array' });
598
599 // Берем первый лист
600 const firstSheetName = workbook.SheetNames[0];
601 const worksheet = workbook.Sheets[firstSheetName];
602
603 // Конвертируем в JSON
604 const jsonData = XLSX.utils.sheet_to_json(worksheet);
605
606 TM_log('XLS parsed:', jsonData.length, 'rows');
607
608 if (jsonData.length === 0) {
609 alert('Файл пустой или не содержит данных');
610 return;
611 }
612
613 // Сохраняем в localStorage
614 saveKnowledgeBase(jsonData);
615 alert(`База знаний загружена успешно!\nЗагружено товаров: ${jsonData.length}`);
616
617 } catch (error) {
618 console.error('Error parsing XLS:', error);
619 alert('Ошибка при чтении файла: ' + error.message);
620 }
621 };
622
623 reader.onerror = function(error) {
624 console.error('Error reading file:', error);
625 alert('Ошибка при чтении файла');
626 };
627
628 reader.readAsArrayBuffer(file);
629 }
630
631 // Функция для добавления стилей массовой генерации
632 function addBulkGenerationStyles() {
633 const styles = `
634 .bulk-generation-panel {
635 margin: 20px 0;
636 padding: 20px;
637 background: #ffffff;
638 border-radius: 8px;
639 border: 2px solid #005bff;
640 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
641 }
642
643 .bulk-generation-title {
644 font-size: 16px;
645 font-weight: 600;
646 margin-bottom: 16px;
647 color: #1a1a1a;
648 }
649
650 .bulk-generation-buttons {
651 display: flex;
652 gap: 12px;
653 margin-bottom: 12px;
654 }
655
656 .bulk-generate-btn, .bulk-answer-all-btn {
657 background: #005bff;
658 color: white;
659 border: none;
660 padding: 12px 24px;
661 border-radius: 6px;
662 font-size: 14px;
663 font-weight: 500;
664 cursor: pointer;
665 transition: background 0.2s;
666 }
667
668 .bulk-generate-btn:hover, .bulk-answer-all-btn:hover {
669 background: #0047cc;
670 }
671
672 .bulk-generate-btn:disabled, .bulk-answer-all-btn:disabled {
673 background: #d1d1d6;
674 cursor: not-allowed;
675 }
676
677 .bulk-answer-all-btn {
678 background: #28a745;
679 }
680
681 .bulk-answer-all-btn:hover {
682 background: #218838;
683 }
684
685 .kb-upload-btn {
686 background: #6f42c1;
687 color: white;
688 border: none;
689 padding: 12px 24px;
690 border-radius: 6px;
691 font-size: 14px;
692 font-weight: 500;
693 cursor: pointer;
694 transition: background 0.2s;
695 }
696
697 .kb-upload-btn:hover {
698 background: #5a32a3;
699 }
700
701 .prompt-edit-btn {
702 background: #fd7e14;
703 color: white;
704 border: none;
705 padding: 12px 24px;
706 border-radius: 6px;
707 font-size: 14px;
708 font-weight: 500;
709 cursor: pointer;
710 transition: background 0.2s;
711 }
712
713 .prompt-edit-btn:hover {
714 background: #e8590c;
715 }
716
717 .model-select-btn {
718 background: #17a2b8;
719 color: white;
720 border: none;
721 padding: 12px 24px;
722 border-radius: 6px;
723 font-size: 14px;
724 font-weight: 500;
725 cursor: pointer;
726 transition: background 0.2s;
727 }
728
729 .model-select-btn:hover {
730 background: #138496;
731 }
732
733 .kb-file-input {
734 display: none;
735 }
736
737 .prompt-modal-overlay {
738 position: fixed;
739 top: 0;
740 left: 0;
741 right: 0;
742 bottom: 0;
743 background: rgba(0, 0, 0, 0.5);
744 display: flex;
745 align-items: center;
746 justify-content: center;
747 z-index: 10000;
748 }
749
750 .prompt-modal {
751 background: white;
752 border-radius: 8px;
753 width: 90%;
754 max-width: 800px;
755 max-height: 90vh;
756 display: flex;
757 flex-direction: column;
758 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
759 }
760
761 .prompt-modal-header {
762 display: flex;
763 justify-content: space-between;
764 align-items: center;
765 padding: 20px;
766 border-bottom: 1px solid #e0e0e0;
767 }
768
769 .prompt-modal-title {
770 margin: 0;
771 font-size: 18px;
772 font-weight: 600;
773 color: #1a1a1a;
774 }
775
776 .prompt-modal-close {
777 background: none;
778 border: none;
779 font-size: 24px;
780 color: #666;
781 cursor: pointer;
782 padding: 0;
783 width: 30px;
784 height: 30px;
785 display: flex;
786 align-items: center;
787 justify-content: center;
788 border-radius: 4px;
789 transition: background 0.2s;
790 }
791
792 .prompt-modal-close:hover {
793 background: #f0f0f0;
794 }
795
796 .prompt-modal-body {
797 padding: 20px;
798 flex: 1;
799 overflow-y: auto;
800 }
801
802 .prompt-modal-hint {
803 font-size: 13px;
804 color: #666;
805 margin-bottom: 12px;
806 padding: 10px;
807 background: #f8f9fa;
808 border-radius: 4px;
809 border-left: 3px solid #fd7e14;
810 }
811
812 .prompt-modal-textarea {
813 width: 100%;
814 min-height: 300px;
815 padding: 12px;
816 border: 1px solid #d1d1d6;
817 border-radius: 6px;
818 font-size: 14px;
819 font-family: 'Courier New', monospace;
820 resize: vertical;
821 box-sizing: border-box;
822 }
823
824 .prompt-modal-textarea:focus {
825 outline: none;
826 border-color: #fd7e14;
827 box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.1);
828 }
829
830 .prompt-modal-footer {
831 display: flex;
832 justify-content: space-between;
833 align-items: center;
834 padding: 20px;
835 border-top: 1px solid #e0e0e0;
836 }
837
838 .prompt-modal-actions {
839 display: flex;
840 gap: 12px;
841 }
842
843 .prompt-modal-reset {
844 background: #6c757d;
845 color: white;
846 border: none;
847 padding: 10px 20px;
848 border-radius: 6px;
849 font-size: 14px;
850 font-weight: 500;
851 cursor: pointer;
852 transition: background 0.2s;
853 }
854
855 .prompt-modal-reset:hover {
856 background: #5a6268;
857 }
858
859 .prompt-modal-cancel {
860 background: #f0f0f0;
861 color: #333;
862 border: none;
863 padding: 10px 20px;
864 border-radius: 6px;
865 font-size: 14px;
866 font-weight: 500;
867 cursor: pointer;
868 transition: background 0.2s;
869 }
870
871 .prompt-modal-cancel:hover {
872 background: #e0e0e0;
873 }
874
875 .prompt-modal-save {
876 background: #fd7e14;
877 color: white;
878 border: none;
879 padding: 10px 20px;
880 border-radius: 6px;
881 font-size: 14px;
882 font-weight: 500;
883 cursor: pointer;
884 transition: background 0.2s;
885 }
886
887 .prompt-modal-save:hover {
888 background: #e8590c;
889 }
890
891 .model-modal-form-group {
892 margin-bottom: 20px;
893 }
894
895 .model-modal-label {
896 display: block;
897 font-size: 14px;
898 font-weight: 600;
899 color: #1a1a1a;
900 margin-bottom: 8px;
901 }
902
903 .model-modal-select {
904 width: 100%;
905 padding: 10px 12px;
906 border: 1px solid #d1d1d6;
907 border-radius: 6px;
908 font-size: 14px;
909 font-family: inherit;
910 box-sizing: border-box;
911 background: white;
912 }
913
914 .model-modal-select:focus {
915 outline: none;
916 border-color: #17a2b8;
917 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
918 }
919
920 .model-modal-input {
921 width: 100%;
922 padding: 10px 12px;
923 border: 1px solid #d1d1d6;
924 border-radius: 6px;
925 font-size: 14px;
926 font-family: inherit;
927 box-sizing: border-box;
928 }
929
930 .model-modal-input:focus {
931 outline: none;
932 border-color: #17a2b8;
933 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
934 }
935
936 .model-modal-input:disabled {
937 background: #f5f5f5;
938 cursor: not-allowed;
939 }
940 `;
941
942 TM_addStyle(styles);
943 }
944
945 // Универсальная функция для поиска контейнера таблицы с вопросами
946 function findQuestionsTableContainer() {
947 console.log('Searching for questions table container...');
948
949 // Стратегия 1: Ищем таблицу по заголовку колонки "Вопрос"
950 const tables = document.querySelectorAll('table');
951 for (const table of tables) {
952 const headers = table.querySelectorAll('th');
953 for (const header of headers) {
954 if (header.textContent.trim() === 'Вопрос') {
955 console.log('Found table by "Вопрос" header');
956 // Ищем самый внешний контейнер с overflow или специфичным классом
957 let container = table;
958 let bestContainer = table.parentElement;
959
960 while (container.parentElement && container.parentElement.tagName !== 'BODY') {
961 container = container.parentElement;
962 const style = window.getComputedStyle(container);
963
964 // Ищем контейнер с overflow: auto или класс, начинающийся с n1d-
965 if ((style.overflow === 'auto' || style.overflowX === 'auto') ||
966 (container.className && container.className.includes('n1d-d') && container.className.includes('k'))) {
967 bestContainer = container;
968 }
969 }
970
971 console.log('Found container:', bestContainer.className);
972 return bestContainer;
973 }
974 }
975 }
976
977 // Стратегия 2: Ищем по data-widget атрибуту
978 const widget = document.querySelector('[data-widget="@seller-ui/reviews"]');
979 if (widget) {
980 const table = widget.querySelector('table');
981 if (table) {
982 console.log('Found table by data-widget');
983 // Ищем ближайший контейнер с классом n1d-
984 let container = table.parentElement;
985 while (container && container !== widget) {
986 if (container.className && container.className.includes('n1d-d') && container.className.includes('k')) {
987 return container;
988 }
989 container = container.parentElement;
990 }
991 return table.parentElement;
992 }
993 }
994
995 // Стратегия 3: Ищем таблицу с tbody, содержащим кнопки с вопросами
996 for (const table of tables) {
997 const questionButtons = table.querySelectorAll('tbody button');
998 if (questionButtons.length > 0) {
999 console.log('Found table by question buttons');
1000 // Ищем родительский контейнер с классом n1d-
1001 let container = table.parentElement;
1002 while (container && container.tagName !== 'BODY') {
1003 if (container.className && container.className.includes('n1d-d') && container.className.includes('k')) {
1004 return container;
1005 }
1006 container = container.parentElement;
1007 }
1008 return table.parentElement;
1009 }
1010 }
1011
1012 // Стратегия 4: Старый способ - по классу (fallback)
1013 const oldContainer = document.querySelector('.n1d-dk9d');
1014 if (oldContainer) {
1015 console.log('Found container by old class selector');
1016 return oldContainer;
1017 }
1018
1019 console.error('Table container not found with any strategy');
1020 return null;
1021 }
1022
1023 // Функция для создания панели массовой генерации
1024 function createBulkGenerationPanel() {
1025 console.log('createBulkGenerationPanel called');
1026 const tableContainer = findQuestionsTableContainer();
1027 console.log('Table container found:', !!tableContainer);
1028 if (!tableContainer) {
1029 console.error('Table container not found');
1030 return;
1031 }
1032
1033 // Проверяем, не создана ли уже панель
1034 const existingPanel = document.querySelector('.bulk-generation-panel');
1035 console.log('Existing panel found:', !!existingPanel);
1036 if (existingPanel) {
1037 console.log('Bulk generation panel already exists');
1038 return;
1039 }
1040
1041 console.log('Creating bulk generation panel...');
1042 const panel = document.createElement('div');
1043 panel.className = 'bulk-generation-panel';
1044
1045 panel.innerHTML = `
1046 <div class="bulk-generation-title">🤖 Массовая генерация ответов</div>
1047 <div class="bulk-generation-buttons">
1048 <button class="bulk-generate-btn">Сгенерировать ответы</button>
1049 <button class="bulk-answer-all-btn" disabled>Ответить всем</button>
1050 <button class="kb-upload-btn">📚 Загрузить базу знаний</button>
1051 <button class="prompt-edit-btn">✏️ Промпт</button>
1052 <button class="model-select-btn">⚙️ Модель</button>
1053 <input type="file" class="kb-file-input" accept=".xls,.xlsx" />
1054 </div>
1055 <div class="bulk-generation-status"></div>
1056 <div class="kb-status">База знаний не загружена</div>
1057 `;
1058
1059 console.log('Inserting panel before table container...');
1060 tableContainer.insertAdjacentElement('beforebegin', panel);
1061 console.log('Panel inserted');
1062
1063 // Добавляем обработчики
1064 const generateBtn = panel.querySelector('.bulk-generate-btn');
1065 const answerAllBtn = panel.querySelector('.bulk-answer-all-btn');
1066 const kbUploadBtn = panel.querySelector('.kb-upload-btn');
1067 const promptEditBtn = panel.querySelector('.prompt-edit-btn');
1068 const kbFileInput = panel.querySelector('.kb-file-input');
1069
1070 generateBtn.addEventListener('click', () => bulkGenerateAnswers());
1071 answerAllBtn.addEventListener('click', () => bulkAnswerAll());
1072
1073 kbUploadBtn.addEventListener('click', () => {
1074 kbFileInput.click();
1075 });
1076
1077 promptEditBtn.addEventListener('click', () => {
1078 showPromptModal();
1079 });
1080
1081 const modelSelectBtn = panel.querySelector('.model-select-btn');
1082
1083 modelSelectBtn.addEventListener('click', () => {
1084 showModelModal();
1085 });
1086
1087 kbFileInput.addEventListener('change', handleFileUpload);
1088
1089 console.log('Bulk generation panel created');
1090
1091 // Загружаем базу знаний из localStorage
1092 loadKnowledgeBase();
1093
1094 // Загружаем кастомный промпт из localStorage
1095 loadCustomPrompt();
1096 }
1097
1098 // Функция для получения всех видимых вопросов
1099 function getVisibleQuestions() {
1100 // Ищем все строки в tbody, которые содержат ссылку на товар и кнопку с вопросом
1101 const allRows = document.querySelectorAll('tbody tr');
1102 const questions = [];
1103
1104 allRows.forEach((row, index) => {
1105 // Ищем ссылку на товар в 3-й колонке
1106 const productLink = row.querySelector('td:nth-child(3) a');
1107 // Ищем кнопку с вопросом в 4-й колонке
1108 const questionButton = row.querySelector('td:nth-child(4) button');
1109
1110 // Ищем SKU - это div с классом body-400 в 3-й колонке
1111 const skuElements = row.querySelectorAll('td:nth-child(3) div');
1112 let sku = '';
1113 for (const el of skuElements) {
1114 const text = el.textContent.trim();
1115 // SKU обычно числовой и длиной 8-12 символов
1116 if (text.length >= 8 && text.length <= 12 && /^\d+$/.test(text)) {
1117 sku = text;
1118 break;
1119 }
1120 }
1121
1122 // Получаем количество ответов из 5-й колонки
1123 const answersCountElement = row.querySelector('td:nth-child(5)');
1124 const answersCount = answersCountElement ? parseInt(answersCountElement.textContent.trim()) : 0;
1125
1126 if (productLink && questionButton) {
1127 questions.push({
1128 index: index,
1129 row: row,
1130 productName: productLink.textContent.trim(),
1131 questionText: questionButton.textContent.trim(),
1132 questionButton: questionButton,
1133 sku: sku,
1134 answersCount: answersCount
1135 });
1136 }
1137 });
1138
1139 return questions;
1140 }
1141
1142 // Функция для массовой генерации ответов
1143 async function bulkGenerateAnswers() {
1144 const generateBtn = document.querySelector('.bulk-generate-btn');
1145 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1146 const statusDiv = document.querySelector('.bulk-generation-status');
1147
1148 try {
1149 generateBtn.disabled = true;
1150 answerAllBtn.disabled = true;
1151 generatedAnswers.clear();
1152
1153 const questions = getVisibleQuestions();
1154
1155 if (questions.length === 0) {
1156 statusDiv.textContent = 'Нет вопросов для обработки';
1157 return;
1158 }
1159
1160 statusDiv.textContent = `Генерация ответов: 0 из ${questions.length}`;
1161
1162 let skippedCount = 0;
1163 let alreadyAnsweredCount = 0;
1164
1165 for (let i = 0; i < questions.length; i++) {
1166 const question = questions[i];
1167
1168 try {
1169 // Пропускаем вопросы, на которые уже есть ответы
1170 if (question.answersCount > 0) {
1171 console.log(`Question ${i + 1} skipped: already has ${question.answersCount} answer(s)`);
1172 alreadyAnsweredCount++;
1173 continue;
1174 }
1175
1176 // Подсвечиваем текущий вопрос
1177 question.row.classList.add('question-row-processing');
1178
1179 statusDiv.textContent = `Генерация ответов: ${i + 1} из ${questions.length}`;
1180
1181 // Генерируем ответ
1182 const answer = await generateAnswerForQuestion(question.productName, question.questionText, question.sku);
1183
1184 // Проверяем валидность ответа
1185 if (!isValidAnswer(answer)) {
1186 console.log(`Question ${i + 1} skipped: AI returned invalid or SKIP answer`);
1187 question.row.classList.remove('question-row-processing');
1188 question.row.classList.add('question-row-error');
1189 skippedCount++;
1190 continue;
1191 }
1192
1193 // Сохраняем ответ
1194 generatedAnswers.set(i, answer);
1195
1196 // Добавляем textarea с ответом
1197 addAnswerTextarea(question.row, answer, i);
1198
1199 // Убираем подсветку обработки и добавляем подсветку завершения
1200 question.row.classList.remove('question-row-processing');
1201 question.row.classList.add('question-row-completed');
1202
1203 console.log(`Answer generated for question ${i + 1}:`, answer);
1204
1205 } catch (error) {
1206 console.error(`Error generating answer for question ${i + 1}:`, error);
1207 TM_log(`ERROR: Error generating answer for question ${i + 1}:`, error.message || error.toString());
1208 question.row.classList.remove('question-row-processing');
1209 question.row.classList.add('question-row-error');
1210 skippedCount++;
1211 // Пропускаем вопрос при ошибке
1212 }
1213 }
1214
1215 let statusText = `Генерация завершена: ${generatedAnswers.size} из ${questions.length} ответов`;
1216 if (alreadyAnsweredCount > 0) {
1217 statusText += ` (уже отвечено: ${alreadyAnsweredCount})`;
1218 }
1219 if (skippedCount > 0) {
1220 statusText += ` (пропущено: ${skippedCount})`;
1221 }
1222 statusDiv.textContent = statusText;
1223 answerAllBtn.disabled = generatedAnswers.size === 0;
1224
1225 } catch (error) {
1226 console.error('Bulk generation error:', error);
1227 statusDiv.textContent = 'Ошибка при массовой генерации';
1228 } finally {
1229 generateBtn.disabled = false;
1230 }
1231 }
1232
1233 // Функция для генерации ответа на один вопрос
1234 async function generateAnswerForQuestion(productName, questionText, sku) {
1235 // Ищем товар в базе знаний по SKU
1236 const productInfo = findProductInKnowledgeBase(sku);
1237 let knowledgeBaseInfo = '';
1238
1239 if (productInfo) {
1240 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
1241 console.log('Using knowledge base info for bulk generation');
1242 }
1243
1244 // Используем кастомный промпт или дефолтный
1245 const promptTemplate = getCurrentPrompt();
1246
1247 // Заменяем переменные в промпте
1248 const prompt = promptTemplate
1249 .replace('{productName}', productName)
1250 .replace('{questionText}', questionText)
1251 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo)
1252 .replace('{additionalPrompt}', '');
1253
1254 const answer = await callAI(prompt);
1255 return answer;
1256 }
1257
1258 // Функция для добавления textarea с ответом
1259 function addAnswerTextarea(row, answer, index) {
1260 // Проверяем, не добавлен ли уже textarea
1261 const existingContainer = row.querySelector('.answer-textarea-container');
1262 if (existingContainer) {
1263 const textarea = existingContainer.querySelector('.answer-textarea');
1264 textarea.value = answer;
1265 autoResizeTextarea(textarea);
1266 return;
1267 }
1268
1269 const questionCell = row.querySelector('td:nth-child(4)');
1270 if (!questionCell) return;
1271
1272 const container = document.createElement('div');
1273 container.className = 'answer-textarea-container';
1274
1275 container.innerHTML = `
1276 <div class="answer-label">Сгенерированный ответ:</div>
1277 <textarea class="answer-textarea" data-question-index="${index}">${answer}</textarea>
1278 `;
1279
1280 questionCell.appendChild(container);
1281
1282 // Обновляем значение в Map при редактировании
1283 const textarea = container.querySelector('.answer-textarea');
1284
1285 // Автоматически изменяем размер textarea
1286 autoResizeTextarea(textarea);
1287
1288 // Предотвращаем открытие модального окна при клике на textarea или контейнер
1289 container.addEventListener('click', (e) => {
1290 e.stopPropagation();
1291 });
1292
1293 textarea.addEventListener('click', (e) => {
1294 e.stopPropagation();
1295 });
1296
1297 textarea.addEventListener('input', () => {
1298 generatedAnswers.set(index, textarea.value);
1299 autoResizeTextarea(textarea);
1300 updateAnswerAllButton();
1301 });
1302
1303 // Активируем кнопку "Ответить всем"
1304 updateAnswerAllButton();
1305 }
1306
1307 // Функция для обновления состояния кнопки "Ответить всем"
1308 function updateAnswerAllButton() {
1309 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1310 if (!answerAllBtn) return;
1311
1312 // Проверяем есть ли хотя бы один ответ
1313 const questions = getVisibleQuestions();
1314 let hasAnswers = false;
1315
1316 for (const question of questions) {
1317 const answerTextarea = question.row.querySelector('.answer-textarea');
1318 if (answerTextarea && answerTextarea.value.trim()) {
1319 hasAnswers = true;
1320 break;
1321 }
1322 }
1323
1324 answerAllBtn.disabled = !hasAnswers;
1325 }
1326
1327 // Функция для автоматического изменения размера textarea
1328 function autoResizeTextarea(textarea) {
1329 // Сбрасываем высоту для правильного расчета
1330 textarea.style.height = 'auto';
1331
1332 // Устанавливаем высоту на основе scrollHeight
1333 const newHeight = Math.max(150, textarea.scrollHeight);
1334 textarea.style.height = newHeight + 'px';
1335
1336 // Также адаптируем ширину, если текст очень длинный
1337 const lineLength = textarea.value.split('\n').reduce((max, line) => Math.max(max, line.length), 0);
1338 if (lineLength > 100) {
1339 textarea.style.width = '100%';
1340 }
1341 }
1342
1343 // Функция для автоматической отправки всех ответов
1344 async function bulkAnswerAll() {
1345 TM_log('=== BULK ANSWER ALL STARTED ===');
1346 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1347 const statusDiv = document.querySelector('.bulk-generation-status');
1348
1349 TM_log('Button found:', !!answerAllBtn);
1350 TM_log('Status div found:', !!statusDiv);
1351
1352 try {
1353 answerAllBtn.disabled = true;
1354 TM_log('Button disabled');
1355
1356 const questions = getVisibleQuestions();
1357 TM_log('Questions found:', questions.length);
1358 let successCount = 0;
1359 let totalToSend = 0;
1360
1361 // Сначала подсчитываем сколько ответов нужно отправить
1362 for (let i = 0; i < questions.length; i++) {
1363 const question = questions[i];
1364 const answerTextarea = question.row.querySelector('.answer-textarea');
1365 if (answerTextarea && answerTextarea.value.trim()) {
1366 totalToSend++;
1367 TM_log(`Question ${i}: has answer, length: ${answerTextarea.value.trim().length}`);
1368 }
1369 }
1370
1371 TM_log('Total answers to send:', totalToSend);
1372
1373 if (totalToSend === 0) {
1374 statusDiv.textContent = 'Нет ответов для отправки';
1375 answerAllBtn.disabled = false;
1376 TM_log('No answers to send, exiting');
1377 return;
1378 }
1379
1380 statusDiv.textContent = `Отправка ответов: 0 из ${totalToSend}`;
1381
1382 for (let i = 0; i < questions.length; i++) {
1383 const question = questions[i];
1384
1385 // Получаем ответ из textarea на странице (не в модальном окне!)
1386 const answerTextareaOnPage = question.row.querySelector('.answer-textarea');
1387 if (!answerTextareaOnPage || !answerTextareaOnPage.value.trim()) {
1388 TM_log(`Question ${i + 1} skipped: no answer`);
1389 continue;
1390 }
1391
1392 const answer = answerTextareaOnPage.value.trim();
1393
1394 try {
1395 TM_log(`Processing question ${i + 1}, answer length:`, answer.length);
1396 statusDiv.textContent = `Отправка ответов: ${successCount + 1} из ${totalToSend}`;
1397
1398 // Кликаем на вопрос
1399 TM_log('About to click question button');
1400 question.questionButton.click();
1401 TM_log('Clicked on question button');
1402
1403 // Ждем открытия модального окна
1404 TM_log('Waiting for modal to open...');
1405 await waitForModal();
1406 TM_log('Modal opened');
1407
1408 // Находим модальное окно - ищем div с текстом "Ответ на вопрос"
1409 const answerSectionTitle = Array.from(document.querySelectorAll('*'))
1410 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1411
1412 if (!answerSectionTitle) {
1413 throw new Error('Answer section not found');
1414 }
1415 TM_log('Answer section title found');
1416
1417 // Находим родительский контейнер с классом mt7
1418 let answerContainer = answerSectionTitle.parentElement;
1419 if (!answerContainer || !answerContainer.classList.contains('mt7')) {
1420 throw new Error('Answer container not found');
1421 }
1422 TM_log('Answer container found');
1423
1424 // Находим textarea внутри этого контейнера - ищем по label "Ваш ответ"
1425 const label = Array.from(answerContainer.querySelectorAll('label'))
1426 .find(l => l.textContent.trim() === 'Ваш ответ');
1427
1428 if (!label) {
1429 throw new Error('Label "Ваш ответ" not found');
1430 }
1431
1432 // Получаем id из атрибута for
1433 const textareaId = label.getAttribute('for');
1434 if (!textareaId) {
1435 throw new Error('Textarea id not found in label');
1436 }
1437 TM_log('Textarea id from label:', textareaId);
1438
1439 // Находим textarea по id
1440 const textareaInModal = document.getElementById(textareaId);
1441 if (!textareaInModal) {
1442 throw new Error('Textarea not found by id: ' + textareaId);
1443 }
1444 TM_log('Textarea found in modal by id');
1445
1446 // Вставляем ответ, который мы взяли СО СТРАНИЦЫ
1447 textareaInModal.value = answer;
1448 TM_log('Answer set to modal textarea, length:', textareaInModal.value.length);
1449
1450 // Генерируем события для обновления состояния
1451 const inputEvent = new Event('input', { bubbles: true });
1452 textareaInModal.dispatchEvent(inputEvent);
1453
1454 const changeEvent = new Event('change', { bubbles: true });
1455 textareaInModal.dispatchEvent(changeEvent);
1456
1457 // Также пробуем установить фокус и снять его для активации валидации
1458 textareaInModal.focus();
1459 textareaInModal.blur();
1460
1461 TM_log('Events dispatched');
1462
1463 // Ждем немного для обновления UI
1464 await sleep(1000);
1465
1466 // Находим кнопку "Отправить ответ" - ищем по тексту внутри кнопки
1467 const submitButton = Array.from(answerContainer.querySelectorAll('button[type="submit"]'))
1468 .find(btn => btn.textContent.includes('Отправить ответ'));
1469
1470 if (!submitButton) {
1471 TM_log('Submit button not found, available buttons:',
1472 Array.from(answerContainer.querySelectorAll('button')).map(b => b.textContent));
1473 throw new Error('Submit button not found');
1474 }
1475 TM_log('Submit button found, text:', submitButton.textContent);
1476 TM_log('Submit button disabled:', submitButton.disabled);
1477
1478 // Если кнопка все еще disabled, пробуем еще раз обновить textarea
1479 if (submitButton.disabled) {
1480 TM_log('Button still disabled, trying to re-trigger events...');
1481 textareaInModal.value = answer;
1482 textareaInModal.dispatchEvent(new Event('input', { bubbles: true }));
1483 textareaInModal.dispatchEvent(new Event('change', { bubbles: true }));
1484 await sleep(500);
1485 TM_log('Submit button disabled after retry:', submitButton.disabled);
1486 }
1487
1488 submitButton.click();
1489 TM_log('Submit button clicked');
1490
1491 // Ждем немного после отправки
1492 await sleep(1500);
1493
1494 // Закрываем модальное окно - ищем кнопку с aria-label "Крестик для закрытия"
1495 const closeButton = Array.from(document.querySelectorAll('button'))
1496 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1497
1498 if (closeButton) {
1499 closeButton.click();
1500 TM_log('Close button clicked');
1501 } else {
1502 TM_log('Close button not found, trying ESC key');
1503 // Альтернативный способ - нажатие ESC
1504 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
1505 }
1506
1507 // Ждем закрытия модального окна
1508 await waitForModalClose();
1509 TM_log('Modal closed');
1510
1511 successCount++;
1512 TM_log(`Answer ${successCount} sent successfully`);
1513
1514 } catch (error) {
1515 TM_log(`ERROR: Error sending answer for question ${i + 1}:`, error.message);
1516 console.error('Full error:', error);
1517 // Закрываем модальное окно при ошибке
1518 const closeButton = Array.from(document.querySelectorAll('button'))
1519 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1520 if (closeButton) {
1521 closeButton.click();
1522 } else {
1523 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
1524 }
1525 await sleep(500);
1526 }
1527 }
1528
1529 statusDiv.textContent = `Отправка завершена: ${successCount} из ${totalToSend} ответов отправлено`;
1530 TM_log('=== BULK ANSWER ALL COMPLETED ===');
1531
1532 } catch (error) {
1533 TM_log('ERROR: Bulk answer error:', error.message);
1534 console.error('Full bulk answer error:', error);
1535 statusDiv.textContent = 'Ошибка при отправке ответов';
1536 } finally {
1537 answerAllBtn.disabled = false;
1538 TM_log('Button re-enabled');
1539 }
1540 }
1541
1542 // Вспомогательная функция ожидания модального окна
1543 function waitForModal() {
1544 return new Promise((resolve) => {
1545 const checkModal = () => {
1546 // Ищем div с текстом "Ответ на вопрос"
1547 const answerSection = Array.from(document.querySelectorAll('*'))
1548 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1549
1550 if (answerSection) {
1551 // Проверяем, что есть textarea с label "Ваш ответ"
1552 const container = answerSection.parentElement;
1553 if (container) {
1554 const label = Array.from(container.querySelectorAll('label'))
1555 .find(l => l.textContent.trim() === 'Ваш ответ');
1556 if (label) {
1557 resolve();
1558 return;
1559 }
1560 }
1561 }
1562 setTimeout(checkModal, 100);
1563 };
1564 checkModal();
1565 });
1566 }
1567
1568 // Вспомогательная функция ожидания закрытия модального окна
1569 function waitForModalClose() {
1570 return new Promise((resolve) => {
1571 const checkModal = () => {
1572 // Проверяем, что модальное окно с вопросом закрыто
1573 const answerSection = Array.from(document.querySelectorAll('*'))
1574 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1575
1576 if (!answerSection) {
1577 resolve();
1578 return;
1579 }
1580
1581 // Проверяем, что есть кнопка закрытия
1582 const closeButton = Array.from(document.querySelectorAll('button'))
1583 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1584
1585 if (closeButton) {
1586 resolve();
1587 return;
1588 }
1589
1590 setTimeout(checkModal, 100);
1591 };
1592 checkModal();
1593 });
1594 }
1595
1596 // Вспомогательная функция задержки
1597 function sleep(ms) {
1598 return new Promise(resolve => setTimeout(resolve, ms));
1599 }
1600
1601 // Функция для загрузки промпта из localStorage
1602 async function loadCustomPrompt() {
1603 try {
1604 const stored = localStorage.getItem('ozon_custom_prompt');
1605 if (stored) {
1606 customPrompt = stored;
1607 console.log('Custom prompt loaded');
1608 }
1609 } catch (error) {
1610 console.error('Error loading custom prompt:', error);
1611 }
1612 }
1613
1614 // Функция для сохранения промпта в localStorage
1615 function saveCustomPrompt(prompt) {
1616 try {
1617 localStorage.setItem('ozon_custom_prompt', prompt);
1618 customPrompt = prompt;
1619 console.log('Custom prompt saved');
1620 } catch (error) {
1621 console.error('Error saving custom prompt:', error);
1622 alert('Ошибка при сохранении промпта: ' + error.message);
1623 }
1624 }
1625
1626 // Функция для получения текущего промпта
1627 function getCurrentPrompt() {
1628 return customPrompt || DEFAULT_PROMPT;
1629 }
1630
1631 // Функция для показа модального окна с промптом
1632 function showPromptModal() {
1633 // Проверяем, не открыто ли уже модальное окно
1634 if (document.querySelector('.prompt-modal-overlay')) {
1635 return;
1636 }
1637
1638 const overlay = document.createElement('div');
1639 overlay.className = 'prompt-modal-overlay';
1640
1641 const modal = document.createElement('div');
1642 modal.className = 'prompt-modal';
1643
1644 const currentPrompt = getCurrentPrompt();
1645
1646 modal.innerHTML = `
1647 <div class="prompt-modal-header">
1648 <h3 class="prompt-modal-title">Редактирование промпта</h3>
1649 <button class="prompt-modal-close">✕</button>
1650 </div>
1651 <div class="prompt-modal-body">
1652 <div class="prompt-modal-hint">
1653 Используйте переменные: {productName}, {questionText}, {knowledgeBaseInfo}
1654 </div>
1655 <textarea class="prompt-modal-textarea">${currentPrompt}</textarea>
1656 </div>
1657 <div class="prompt-modal-footer">
1658 <button class="prompt-modal-reset">Сбросить на дефолтный</button>
1659 <div class="prompt-modal-actions">
1660 <button class="prompt-modal-cancel">Отмена</button>
1661 <button class="prompt-modal-save">Сохранить</button>
1662 </div>
1663 </div>
1664 `;
1665
1666 overlay.appendChild(modal);
1667 document.body.appendChild(overlay);
1668
1669 // Обработчики
1670 const closeBtn = modal.querySelector('.prompt-modal-close');
1671 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1672 const saveBtn = modal.querySelector('.prompt-modal-save');
1673 const resetBtn = modal.querySelector('.prompt-modal-reset');
1674 const textarea = modal.querySelector('.prompt-modal-textarea');
1675
1676 const closeModal = () => {
1677 overlay.remove();
1678 };
1679
1680 closeBtn.addEventListener('click', closeModal);
1681 cancelBtn.addEventListener('click', closeModal);
1682 overlay.addEventListener('click', (e) => {
1683 if (e.target === overlay) closeModal();
1684 });
1685
1686 saveBtn.addEventListener('click', () => {
1687 const newPrompt = textarea.value.trim();
1688 if (newPrompt) {
1689 saveCustomPrompt(newPrompt);
1690 alert('Промпт сохранен успешно!');
1691 closeModal();
1692 } else {
1693 alert('Промпт не может быть пустым');
1694 }
1695 });
1696
1697 resetBtn.addEventListener('click', () => {
1698 if (confirm('Вы уверены, что хотите сбросить промпт на дефолтный?')) {
1699 textarea.value = DEFAULT_PROMPT;
1700 localStorage.removeItem('ozon_custom_prompt');
1701 customPrompt = null;
1702 alert('Промпт сброшен на дефолтный');
1703 }
1704 });
1705 }
1706
1707 // Функция для показа модального окна выбора модели
1708 function showModelModal() {
1709 // Проверяем, не открыто ли уже модальное окно
1710 if (document.querySelector('.prompt-modal-overlay')) {
1711 return;
1712 }
1713
1714 // Загружаем настройки модели из localStorage
1715 loadModelSettings();
1716
1717 const overlay = document.createElement('div');
1718 overlay.className = 'prompt-modal-overlay';
1719
1720 const modal = document.createElement('div');
1721 modal.className = 'prompt-modal';
1722
1723 modal.innerHTML = `
1724 <div class="prompt-modal-header">
1725 <h3 class="prompt-modal-title">Настройки AI модели</h3>
1726 <button class="prompt-modal-close">✕</button>
1727 </div>
1728 <div class="prompt-modal-body">
1729 <div class="model-modal-form-group">
1730 <label class="model-modal-label">Провайдер:</label>
1731 <select class="model-modal-select" id="model-provider-select">
1732 <option value="rmcall" ${modelSettings.provider === 'rmcall' ? 'selected' : ''}>RM Call (стандартный)</option>
1733 <option value="openrouter" ${modelSettings.provider === 'openrouter' ? 'selected' : ''}>OpenRouter</option>
1734 </select>
1735 </div>
1736
1737 <div class="model-modal-form-group" id="model-select-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1738 <label class="model-modal-label">
1739 Модель:
1740 <button class="model-test-btn" id="test-models-btn" style="margin-left: 10px; padding: 4px 12px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
1741 🔍 Проверить модели
1742 </button>
1743 </label>
1744 <select class="model-modal-select" id="model-name-select" size="8" style="height: auto;">
1745 ${getModelOptions()}
1746 </select>
1747 <div id="model-test-status" style="margin-top: 8px; font-size: 12px; color: #666;"></div>
1748 <div style="margin-top: 12px; display: flex; gap: 8px;">
1749 <input type="text" id="custom-model-input" placeholder="Введите ID модели (например, nvidia/nemotron-3-super-120b-a12b:free)" style="flex: 1; padding: 8px; border: 1px solid #d1d1d6; border-radius: 4px; font-size: 13px;" />
1750 <button id="add-model-btn" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">➕ Добавить</button>
1751 <button id="remove-model-btn" style="padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">🗑️ Удалить</button>
1752 </div>
1753 </div>
1754
1755 <div class="model-modal-form-group" id="api-key-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1756 <label class="model-modal-label">API ключ OpenRouter:</label>
1757 <input type="password" class="model-modal-input" id="api-key-input" placeholder="Введите API ключ" value="${modelSettings.apiKey || ''}" />
1758 <div class="prompt-modal-hint" style="margin-top: 8px;">
1759 Получите бесплатный API ключ на <a href="https://openrouter.ai/keys" target="_blank" style="color: #17a2b8;">openrouter.ai/keys</a>
1760 </div>
1761 </div>
1762 </div>
1763 <div class="prompt-modal-footer">
1764 <div></div>
1765 <div class="prompt-modal-actions">
1766 <button class="prompt-modal-cancel">Отмена</button>
1767 <button class="prompt-modal-save">Сохранить</button>
1768 </div>
1769 </div>
1770 `;
1771
1772 overlay.appendChild(modal);
1773 document.body.appendChild(overlay);
1774
1775 // Обработчики
1776 const closeBtn = modal.querySelector('.prompt-modal-close');
1777 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1778 const saveBtn = modal.querySelector('.prompt-modal-save');
1779 const providerSelect = modal.querySelector('#model-provider-select');
1780 const modelNameSelect = modal.querySelector('#model-name-select');
1781 const apiKeyInput = modal.querySelector('#api-key-input');
1782 const modelSelectGroup = modal.querySelector('#model-select-group');
1783 const apiKeyGroup = modal.querySelector('#api-key-group');
1784 const testModelsBtn = modal.querySelector('#test-models-btn');
1785
1786 // Устанавливаем текущую модель
1787 if (modelSettings.model) {
1788 modelNameSelect.value = modelSettings.model;
1789 }
1790
1791 const closeModal = () => {
1792 overlay.remove();
1793 };
1794
1795 // Обработчик изменения провайдера
1796 providerSelect.addEventListener('change', () => {
1797 const isOpenRouter = providerSelect.value === 'openrouter';
1798 modelSelectGroup.style.display = isOpenRouter ? 'block' : 'none';
1799 apiKeyGroup.style.display = isOpenRouter ? 'block' : 'none';
1800 });
1801
1802 // Обработчик проверки моделей
1803 testModelsBtn.addEventListener('click', async () => {
1804 await testAllModels(modelNameSelect, apiKeyInput.value.trim());
1805 });
1806
1807 // Обработчик добавления модели
1808 const addModelBtn = modal.querySelector('#add-model-btn');
1809 const removeModelBtn = modal.querySelector('#remove-model-btn');
1810 const customModelInput = modal.querySelector('#custom-model-input');
1811
1812 addModelBtn.addEventListener('click', () => {
1813 const modelId = customModelInput.value.trim();
1814 if (!modelId) {
1815 alert('Пожалуйста, введите ID модели');
1816 return;
1817 }
1818
1819 // Проверяем, не добавлена ли уже эта модель
1820 if (!modelSettings.customModels) {
1821 modelSettings.customModels = [];
1822 }
1823
1824 if (modelSettings.customModels.includes(modelId)) {
1825 alert('Эта модель уже добавлена');
1826 return;
1827 }
1828
1829 // Добавляем модель
1830 modelSettings.customModels.push(modelId);
1831 saveModelSettings();
1832
1833 // Обновляем список моделей
1834 modelNameSelect.innerHTML = getModelOptions();
1835 modelNameSelect.value = modelId;
1836
1837 customModelInput.value = '';
1838 alert('Модель добавлена успешно!');
1839 });
1840
1841 removeModelBtn.addEventListener('click', () => {
1842 const selectedModel = modelNameSelect.value;
1843 if (!selectedModel) {
1844 alert('Пожалуйста, выберите модель для удаления');
1845 return;
1846 }
1847
1848 // Проверяем, является ли модель кастомной
1849 if (!modelSettings.customModels || !modelSettings.customModels.includes(selectedModel)) {
1850 alert('Можно удалять только добавленные вами модели');
1851 return;
1852 }
1853
1854 if (!confirm(`Вы уверены, что хотите удалить модель "${selectedModel}"?`)) {
1855 return;
1856 }
1857
1858 // Удаляем модель
1859 modelSettings.customModels = modelSettings.customModels.filter(m => m !== selectedModel);
1860 saveModelSettings();
1861
1862 // Обновляем список моделей
1863 modelNameSelect.innerHTML = getModelOptions();
1864
1865 alert('Модель удалена успешно!');
1866 });
1867
1868 closeBtn.addEventListener('click', closeModal);
1869 cancelBtn.addEventListener('click', closeModal);
1870 overlay.addEventListener('click', (e) => {
1871 if (e.target === overlay) closeModal();
1872 });
1873
1874 saveBtn.addEventListener('click', () => {
1875 const provider = providerSelect.value;
1876 const model = modelNameSelect.value;
1877 const apiKey = apiKeyInput.value.trim();
1878
1879 // Валидация
1880 if (provider === 'openrouter' && !apiKey) {
1881 alert('Пожалуйста, введите API ключ для OpenRouter');
1882 return;
1883 }
1884
1885 // Сохраняем настройки
1886 modelSettings.provider = provider;
1887 modelSettings.model = model;
1888 modelSettings.apiKey = apiKey;
1889
1890 saveModelSettings();
1891 alert('Настройки модели сохранены успешно!');
1892 closeModal();
1893 });
1894 }
1895
1896 // Функция для тестирования всех моделей
1897 async function testAllModels(selectElement, apiKey) {
1898 if (!apiKey) {
1899 alert('Пожалуйста, введите API ключ для проверки моделей');
1900 return;
1901 }
1902
1903 const statusDiv = document.querySelector('#model-test-status');
1904 const testBtn = document.querySelector('#test-models-btn');
1905
1906 testBtn.disabled = true;
1907 testBtn.textContent = '⏳ Проверка...';
1908 statusDiv.textContent = 'Проверка моделей...';
1909
1910 const models = [];
1911 for (let i = 0; i < selectElement.options.length; i++) {
1912 models.push({
1913 value: selectElement.options[i].value,
1914 text: selectElement.options[i].text,
1915 option: selectElement.options[i]
1916 });
1917 }
1918
1919 let workingCount = 0;
1920 let failedCount = 0;
1921
1922 for (let i = 0; i < models.length; i++) {
1923 const model = models[i];
1924 statusDiv.textContent = `Проверка ${i + 1} из ${models.length}: ${model.text}`;
1925
1926 try {
1927 const isWorking = await testModel(model.value, apiKey);
1928
1929 if (isWorking) {
1930 model.option.text = '✅ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1931 workingCount++;
1932 } else {
1933 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1934 failedCount++;
1935 }
1936 } catch (error) {
1937 console.error(`Model ${model.value} test failed:`, error);
1938 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1939 failedCount++;
1940 }
1941
1942 // Небольшая задержка между запросами
1943 await sleep(500);
1944 }
1945
1946 testBtn.disabled = false;
1947 testBtn.textContent = '🔍 Проверить модели';
1948 statusDiv.innerHTML = `<span style="color: #28a745;">✅ Работает: ${workingCount}</span> | <span style="color: #dc3545;">❌ Не работает: ${failedCount}</span>`;
1949 }
1950
1951 // Функция для тестирования одной модели
1952 async function testModel(modelName, apiKey) {
1953 try {
1954 const response = await GM.xmlhttpRequest({
1955 method: 'POST',
1956 url: 'https://openrouter.ai/api/v1/chat/completions',
1957 headers: {
1958 'Content-Type': 'application/json',
1959 'Authorization': `Bearer ${apiKey}`,
1960 'HTTP-Referer': window.location.href,
1961 'X-Title': 'Ozon AI Answer Generator'
1962 },
1963 data: JSON.stringify({
1964 model: modelName,
1965 messages: [
1966 {
1967 role: 'user',
1968 content: 'Ответь одним словом: привет'
1969 }
1970 ],
1971 max_tokens: 10
1972 }),
1973 timeout: 10000
1974 });
1975
1976 if (response.status === 200) {
1977 const data = JSON.parse(response.responseText);
1978 return !!(data.choices && data.choices[0] && data.choices[0].message);
1979 }
1980
1981 return false;
1982 } catch (error) {
1983 console.error(`Model ${modelName} test failed:`, error);
1984 return false;
1985 }
1986 }
1987
1988 // Функция для загрузки настроек модели из localStorage
1989 function loadModelSettings() {
1990 try {
1991 const stored = localStorage.getItem('ozon_model_settings');
1992 if (stored) {
1993 const settings = JSON.parse(stored);
1994 modelSettings.provider = settings.provider || 'rmcall';
1995 modelSettings.model = settings.model || 'google/gemini-2.0-flash-exp:free';
1996 modelSettings.apiKey = settings.apiKey || '';
1997 modelSettings.customModels = settings.customModels || [];
1998 console.log('Model settings loaded:', modelSettings);
1999 }
2000 } catch (error) {
2001 console.error('Error loading model settings:', error);
2002 }
2003 }
2004
2005 // Функция для генерации списка моделей
2006 function getModelOptions() {
2007 const defaultModels = [
2008 { value: 'arcee-ai/trinity-large-preview:free', label: 'Arcee Trinity Large (бесплатно)' },
2009 { value: 'openrouter/free', label: 'OpenRouter Free' }
2010 ];
2011
2012 let options = '';
2013
2014 // Добавляем дефолтные модели
2015 for (const model of defaultModels) {
2016 options += `<option value="${model.value}">${model.label}</option>`;
2017 }
2018
2019 // Добавляем кастомные модели
2020 if (modelSettings.customModels && modelSettings.customModels.length > 0) {
2021 options += '<option disabled>──────────</option>';
2022 for (const modelId of modelSettings.customModels) {
2023 options += `<option value="${modelId}">🔧 ${modelId}</option>`;
2024 }
2025 }
2026
2027 return options;
2028 }
2029
2030 // Функция для сохранения настроек модели в localStorage
2031 function saveModelSettings() {
2032 try {
2033 localStorage.setItem('ozon_model_settings', JSON.stringify(modelSettings));
2034 console.log('Model settings saved:', modelSettings);
2035 } catch (error) {
2036 console.error('Error saving model settings:', error);
2037 alert('Ошибка при сохранении настроек модели: ' + error.message);
2038 }
2039 }
2040
2041 // Универсальная функция для вызова AI
2042 async function callAI(prompt) {
2043 // Загружаем настройки модели
2044 loadModelSettings();
2045
2046 console.log('Using AI provider:', modelSettings.provider);
2047 console.log('Prompt being sent to AI:', prompt);
2048
2049 if (modelSettings.provider === 'openrouter') {
2050 // Используем OpenRouter API
2051 if (!modelSettings.apiKey) {
2052 throw new Error('API ключ OpenRouter не настроен. Откройте настройки модели и введите API ключ.');
2053 }
2054
2055 console.log('Calling OpenRouter API with model:', modelSettings.model);
2056
2057 try {
2058 const response = await GM.xmlhttpRequest({
2059 method: 'POST',
2060 url: 'https://openrouter.ai/api/v1/chat/completions',
2061 headers: {
2062 'Content-Type': 'application/json',
2063 'Authorization': `Bearer ${modelSettings.apiKey}`,
2064 'HTTP-Referer': window.location.href,
2065 'X-Title': 'Ozon AI Answer Generator'
2066 },
2067 data: JSON.stringify({
2068 model: modelSettings.model,
2069 messages: [
2070 {
2071 role: 'user',
2072 content: prompt
2073 }
2074 ]
2075 })
2076 });
2077
2078 console.log('OpenRouter response status:', response.status);
2079 console.log('OpenRouter response:', response.responseText);
2080
2081 if (response.status !== 200) {
2082 console.error('OpenRouter API error:', response);
2083 throw new Error(`OpenRouter API error: ${response.status} - ${response.statusText}`);
2084 }
2085
2086 const data = JSON.parse(response.responseText);
2087
2088 if (!data.choices || !data.choices[0] || !data.choices[0].message) {
2089 console.error('Invalid OpenRouter response:', data);
2090 throw new Error('Неверный формат ответа от OpenRouter API');
2091 }
2092
2093 return data.choices[0].message.content;
2094
2095 } catch (error) {
2096 console.error('OpenRouter API call failed:', error);
2097 throw new Error(`Ошибка вызова OpenRouter API: ${error.message}`);
2098 }
2099 } else {
2100 // Используем стандартный RM.aiCall
2101 console.log('Calling RM.aiCall');
2102 return await RM.aiCall(prompt);
2103 }
2104 }
2105
2106 // Инициализация массовой генерации
2107 function initBulkGeneration() {
2108 // Проверяем, что мы на странице со списком вопросов
2109 if (!window.location.href.includes('/app/reviews/questions')) {
2110 return;
2111 }
2112
2113 console.log('Initializing bulk generation...');
2114
2115 addBulkGenerationStyles();
2116
2117 // Ждем загрузки таблицы
2118 const observer = new MutationObserver(debounce(() => {
2119 const tableContainer = findQuestionsTableContainer();
2120 const existingPanel = document.querySelector('.bulk-generation-panel');
2121
2122 if (tableContainer) {
2123 // Если панель существует, но находится не перед контейнером таблицы
2124 if (existingPanel) {
2125 // Проверяем, находится ли панель перед текущим контейнером таблицы
2126 const panelNextSibling = existingPanel.nextElementSibling;
2127 if (panelNextSibling !== tableContainer) {
2128 console.log('Panel found but in wrong position, moving it...');
2129 tableContainer.insertAdjacentElement('beforebegin', existingPanel);
2130 console.log('Panel moved to correct position');
2131 }
2132 } else {
2133 // Панели нет, создаем новую
2134 console.log('Panel not found, creating new one...');
2135 createBulkGenerationPanel();
2136 }
2137 }
2138 }, 300));
2139
2140 observer.observe(document.body, {
2141 childList: true,
2142 subtree: true
2143 });
2144
2145 // Пробуем создать панель сразу
2146 const tableContainer = findQuestionsTableContainer();
2147 if (tableContainer) {
2148 createBulkGenerationPanel();
2149 }
2150 }
2151
2152 // Запускаем инициализацию массовой генерации
2153 initBulkGeneration();
2154
2155})();