Size
128.6 KB
Version
1.3.67
Created
Mar 25, 2026
Updated
22 days ago
1// ==UserScript==
2// @name Ozon AI Answer Generator
3// @description AI-powered answer generator for Ozon seller questions
4// @version 1.3.67
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 reviewsPrompt = null;
452
453 // Хранилище настроек модели
454 let modelSettings = {
455 provider: 'rmcall', // 'rmcall' или 'openrouter'
456 model: 'google/gemini-2.0-flash-exp:free',
457 apiKey: '',
458 customModels: [] // Список кастомных моделей
459 };
460
461 // Дефолтный промпт
462 const DEFAULT_PROMPT = 'Ты - профессиональный менеджер по работе с клиентами на маркетплейсе Ozon.\n\n' +
463 'Продукт: {productName}\n\n' +
464 'Вопрос покупателя: {questionText}{knowledgeBaseInfo}\n\n' +
465 'Задача: Сгенерируй профессиональный, вежливый и информативный ответ на вопрос покупателя о товаре.\n\n' +
466 'Требования к ответу:\n' +
467 '- Будь вежливым и дружелюбным\n' +
468 '- Отвечай по существу вопроса\n' +
469 '- Используй информацию о продукте\n' +
470 '- Ответ должен быть кратким (2-4 предложения)\n' +
471 '- Не придумывай характеристики, которых нет в названии продукта\n' +
472 '- Если нужна дополнительная информация, вежливо предложи обратиться к описанию товара\n' +
473 '- Учитывай при ответе Дополнительные инструкции {additionalPrompt} если они есть\n' +
474 '- ВАЖНО: Если у тебя недостаточно информации для ответа или ты не знаешь что ответить, напиши ТОЛЬКО слово "SKIP" без дополнительных объяснений';
475
476 // Дефолтный промпт для отзывов
477 const DEFAULT_REVIEWS_PROMPT = 'Ты - профессиональный менеджер по работе с клиентами на маркетплейсе Ozon.\n\n' +
478 'Продукт: {productName}\n\n' +
479 'Оценка: {rating} звезд\n\n' +
480 'Комментарий покупателя: {comment}{knowledgeBaseInfo}\n\n' +
481 'Задача: Сгенерируй профессиональный, вежливый и благодарный ответ на отзыв покупателя.\n\n' +
482 'Требования к ответу:\n' +
483 '- Будь вежливым и благодарным\n' +
484 '- Если оценка 4-5 звезд и нет комментария - просто поблагодари за высокую оценку\n' +
485 '- Если оценка 3 звезды и ниже без комментария - вырази сожаление и предложи написать в чат для решения проблем\n' +
486 '- Если есть комментарий - отвечай по существу, используя информацию о продукте\n' +
487 '- Ответ должен быть кратким (2-3 предложения)\n' +
488 '- Не придумывай характеристики, которых нет в информации о продукте\n' +
489 '- ВАЖНО: Если у тебя недостаточно информации для ответа, напиши ТОЛЬКО слово "SKIP" без дополнительных объяснений';
490
491 // Функция для загрузки базы знаний из localStorage
492 async function loadKnowledgeBase() {
493 try {
494 const stored = localStorage.getItem('ozon_knowledge_base');
495 if (stored) {
496 knowledgeBase = JSON.parse(stored);
497 TM_log('Knowledge base loaded:', knowledgeBase.length, 'products');
498 updateKnowledgeBaseStatus();
499 }
500 } catch (error) {
501 console.error('Error loading knowledge base:', error);
502 }
503 }
504
505 // Функция для сохранения базы знаний в localStorage
506 function saveKnowledgeBase(data) {
507 try {
508 localStorage.setItem('ozon_knowledge_base', JSON.stringify(data));
509 knowledgeBase = data;
510 TM_log('Knowledge base saved:', data.length, 'products');
511 updateKnowledgeBaseStatus();
512 } catch (error) {
513 console.error('Error saving knowledge base:', error);
514 alert('Ошибка при сохранении базы знаний: ' + error.message);
515 }
516 }
517
518 // Функция для обновления статуса базы знаний
519 function updateKnowledgeBaseStatus() {
520 const statusDiv = document.querySelector('.kb-status');
521 if (statusDiv) {
522 if (knowledgeBase && knowledgeBase.length > 0) {
523 statusDiv.textContent = `База знаний загружена: ${knowledgeBase.length} товаров`;
524 statusDiv.style.color = '#28a745';
525 } else {
526 statusDiv.textContent = 'База знаний не загружена';
527 statusDiv.style.color = '#666';
528 }
529 }
530 }
531
532 // Функция для поиска товара в базе знаний по SKU
533 function findProductInKnowledgeBase(sellerArticle) {
534 if (!knowledgeBase || knowledgeBase.length === 0) {
535 TM_log('Knowledge base is empty');
536 return null;
537 }
538
539 if (!sellerArticle) {
540 TM_log('Seller article is empty');
541 return null;
542 }
543
544 TM_log('Searching for seller article:', sellerArticle);
545
546 // Ищем товар по артикулу - проверяем несколько возможных названий колонок
547 const possibleColumns = [
548 'Штрихкод (Серийный номер / EAN)',
549 'SKU',
550 'Артикул',
551 'Штрихкод',
552 'Серийный номер',
553 'EAN',
554 'Артикул продавца'
555 ];
556
557 let foundProduct = null;
558 let foundColumn = '';
559
560 for (const item of knowledgeBase) {
561 for (const column of possibleColumns) {
562 const itemSku = item[column];
563 if (itemSku && itemSku.toString().trim() === sellerArticle) {
564 foundProduct = item;
565 foundColumn = column;
566 break;
567 }
568 }
569 if (foundProduct) break;
570 }
571
572 if (foundProduct) {
573 TM_log(`Product found in knowledge base using column "${foundColumn}"`);
574 TM_log('Product data:', foundProduct);
575 } else {
576 TM_log('Product not found in knowledge base for article:', sellerArticle);
577 TM_log('Available columns in first product:', knowledgeBase[0] ? Object.keys(knowledgeBase[0]) : 'No products');
578 }
579
580 return foundProduct;
581 }
582
583 // Функция для форматирования информации о товаре из базы знаний
584 function formatProductInfo(product) {
585 const info = [];
586
587 if (product['Название товара']) info.push(`Название: ${product['Название товара']}`);
588 if (product['Бренд*']) info.push(`Бренд: ${product['Бренд*']}`);
589 if (product['Тип*']) info.push(`Тип: ${product['Тип*']}`);
590 if (product['Состав*']) info.push(`Состав: ${product['Состав*']}`);
591 if (product['Аннотация']) info.push(`Описание: ${product['Аннотация']}`);
592 if (product['Целевая аудитория']) info.push(`Целевая аудитория: ${product['Целевая аудитория']}`);
593 if (product['Направление БАД']) info.push(`Направление: ${product['Направление БАД']}`);
594 if (product['Вкусовой акцент (вкус)']) info.push(`Вкус: ${product['Вкусовой акцент (вкус)']}`);
595 if (product['Форма выпуска продукта']) info.push(`Форма выпуска: ${product['Форма выпуска продукта']}`);
596 if (product['Способ применения']) info.push(`Способ применения: ${product['Способ применения']}`);
597 if (product['Срок годности в днях']) info.push(`Срок годности: ${product['Срок годности в днях']} дней`);
598 if (product['Страна-изготовитель']) info.push(`Страна-изготовитель: ${product['Страна-изготовитель']}`);
599 if (product['Для детей']) info.push(`Для детей: ${product['Для детей']}`);
600 if (product['Минимальный возраст от']) info.push(`Минимальный возраст: ${product['Минимальный возраст от']}`);
601
602 return info.join('\n');
603 }
604
605 // Функция для обработки загрузки XLS файла
606 function handleFileUpload(event) {
607 const file = event.target.files[0];
608 if (!file) return;
609
610 const reader = new FileReader();
611
612 reader.onload = function(e) {
613 try {
614 const data = new Uint8Array(e.target.result);
615 const workbook = XLSX.read(data, { type: 'array' });
616
617 // Берем первый лист
618 const firstSheetName = workbook.SheetNames[0];
619 const worksheet = workbook.Sheets[firstSheetName];
620
621 // Конвертируем в JSON
622 const jsonData = XLSX.utils.sheet_to_json(worksheet);
623
624 TM_log('XLS parsed:', jsonData.length, 'rows');
625
626 if (jsonData.length === 0) {
627 alert('Файл пустой или не содержит данных');
628 return;
629 }
630
631 // Сохраняем в localStorage
632 saveKnowledgeBase(jsonData);
633 alert(`База знаний загружена успешно!\nЗагружено товаров: ${jsonData.length}`);
634
635 } catch (error) {
636 console.error('Error parsing XLS:', error);
637 alert('Ошибка при чтении файла: ' + error.message);
638 }
639 };
640
641 reader.onerror = function(error) {
642 console.error('Error reading file:', error);
643 alert('Ошибка при чтении файла');
644 };
645
646 reader.readAsArrayBuffer(file);
647 }
648
649 // Функция для добавления стилей массовой генерации
650 function addBulkGenerationStyles() {
651 const styles = `
652 .bulk-generation-panel {
653 margin: 20px 0;
654 padding: 20px;
655 background: #ffffff;
656 border-radius: 8px;
657 border: 2px solid #005bff;
658 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
659 }
660
661 .bulk-generation-title {
662 font-size: 16px;
663 font-weight: 600;
664 margin-bottom: 16px;
665 color: #1a1a1a;
666 }
667
668 .bulk-generation-buttons {
669 display: flex;
670 gap: 12px;
671 margin-bottom: 12px;
672 flex-wrap: wrap;
673 }
674
675 .bulk-generate-btn, .bulk-answer-all-btn {
676 background: #005bff;
677 color: white;
678 border: none;
679 padding: 12px 24px;
680 border-radius: 6px;
681 font-size: 14px;
682 font-weight: 500;
683 cursor: pointer;
684 transition: background 0.2s;
685 }
686
687 .bulk-generate-btn:hover, .bulk-answer-all-btn:hover {
688 background: #0047cc;
689 }
690
691 .bulk-generate-btn:disabled, .bulk-answer-all-btn:disabled {
692 background: #d1d1d6;
693 cursor: not-allowed;
694 }
695
696 .bulk-answer-all-btn {
697 background: #28a745;
698 }
699
700 .bulk-answer-all-btn:hover {
701 background: #218838;
702 }
703
704 .kb-upload-btn {
705 background: #6f42c1;
706 color: white;
707 border: none;
708 padding: 12px 24px;
709 border-radius: 6px;
710 font-size: 14px;
711 font-weight: 500;
712 cursor: pointer;
713 transition: background 0.2s;
714 }
715
716 .kb-upload-btn:hover {
717 background: #5a32a3;
718 }
719
720 .prompt-edit-btn {
721 background: #fd7e14;
722 color: white;
723 border: none;
724 padding: 12px 24px;
725 border-radius: 6px;
726 font-size: 14px;
727 font-weight: 500;
728 cursor: pointer;
729 transition: background 0.2s;
730 }
731
732 .prompt-edit-btn:hover {
733 background: #e8590c;
734 }
735
736 .model-select-btn {
737 background: #17a2b8;
738 color: white;
739 border: none;
740 padding: 12px 24px;
741 border-radius: 6px;
742 font-size: 14px;
743 font-weight: 500;
744 cursor: pointer;
745 transition: background 0.2s;
746 }
747
748 .model-select-btn:hover {
749 background: #138496;
750 }
751
752 .kb-file-input {
753 display: none;
754 }
755
756 .prompt-modal-overlay {
757 position: fixed;
758 top: 0;
759 left: 0;
760 right: 0;
761 bottom: 0;
762 background: rgba(0, 0, 0, 0.5);
763 display: flex;
764 align-items: center;
765 justify-content: center;
766 z-index: 10000;
767 }
768
769 .prompt-modal {
770 background: white;
771 border-radius: 8px;
772 width: 90%;
773 max-width: 800px;
774 max-height: 90vh;
775 display: flex;
776 flex-direction: column;
777 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
778 }
779
780 .prompt-modal-header {
781 display: flex;
782 justify-content: space-between;
783 align-items: center;
784 padding: 20px;
785 border-bottom: 1px solid #e0e0e0;
786 }
787
788 .prompt-modal-title {
789 margin: 0;
790 font-size: 18px;
791 font-weight: 600;
792 color: #1a1a1a;
793 }
794
795 .prompt-modal-close {
796 background: none;
797 border: none;
798 font-size: 24px;
799 color: #666;
800 cursor: pointer;
801 padding: 0;
802 width: 30px;
803 height: 30px;
804 display: flex;
805 align-items: center;
806 justify-content: center;
807 border-radius: 4px;
808 transition: background 0.2s;
809 }
810
811 .prompt-modal-close:hover {
812 background: #f0f0f0;
813 }
814
815 .prompt-modal-body {
816 padding: 20px;
817 flex: 1;
818 overflow-y: auto;
819 }
820
821 .prompt-modal-hint {
822 font-size: 13px;
823 color: #666;
824 margin-bottom: 12px;
825 padding: 10px;
826 background: #f8f9fa;
827 border-radius: 4px;
828 border-left: 3px solid #fd7e14;
829 }
830
831 .prompt-modal-textarea {
832 width: 100%;
833 min-height: 300px;
834 padding: 12px;
835 border: 1px solid #d1d1d6;
836 border-radius: 6px;
837 font-size: 14px;
838 font-family: 'Courier New', monospace;
839 resize: vertical;
840 box-sizing: border-box;
841 }
842
843 .prompt-modal-textarea:focus {
844 outline: none;
845 border-color: #fd7e14;
846 box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.1);
847 }
848
849 .prompt-modal-footer {
850 display: flex;
851 justify-content: space-between;
852 align-items: center;
853 padding: 20px;
854 border-top: 1px solid #e0e0e0;
855 }
856
857 .prompt-modal-actions {
858 display: flex;
859 gap: 12px;
860 }
861
862 .prompt-modal-reset {
863 background: #6c757d;
864 color: white;
865 border: none;
866 padding: 10px 20px;
867 border-radius: 6px;
868 font-size: 14px;
869 font-weight: 500;
870 cursor: pointer;
871 transition: background 0.2s;
872 }
873
874 .prompt-modal-reset:hover {
875 background: #5a6268;
876 }
877
878 .prompt-modal-cancel {
879 background: #f0f0f0;
880 color: #333;
881 border: none;
882 padding: 10px 20px;
883 border-radius: 6px;
884 font-size: 14px;
885 font-weight: 500;
886 cursor: pointer;
887 transition: background 0.2s;
888 }
889
890 .prompt-modal-cancel:hover {
891 background: #e0e0e0;
892 }
893
894 .prompt-modal-save {
895 background: #fd7e14;
896 color: white;
897 border: none;
898 padding: 10px 20px;
899 border-radius: 6px;
900 font-size: 14px;
901 font-weight: 500;
902 cursor: pointer;
903 transition: background 0.2s;
904 }
905
906 .prompt-modal-save:hover {
907 background: #e8590c;
908 }
909
910 .model-modal-form-group {
911 margin-bottom: 20px;
912 }
913
914 .model-modal-label {
915 display: block;
916 font-size: 14px;
917 font-weight: 600;
918 color: #1a1a1a;
919 margin-bottom: 8px;
920 }
921
922 .model-modal-select {
923 width: 100%;
924 padding: 10px 12px;
925 border: 1px solid #d1d1d6;
926 border-radius: 6px;
927 font-size: 14px;
928 font-family: inherit;
929 box-sizing: border-box;
930 background: white;
931 }
932
933 .model-modal-select:focus {
934 outline: none;
935 border-color: #17a2b8;
936 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
937 }
938
939 .model-modal-input {
940 width: 100%;
941 padding: 10px 12px;
942 border: 1px solid #d1d1d6;
943 border-radius: 6px;
944 font-size: 14px;
945 font-family: inherit;
946 box-sizing: border-box;
947 }
948
949 .model-modal-input:focus {
950 outline: none;
951 border-color: #17a2b8;
952 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
953 }
954
955 .model-modal-input:disabled {
956 background: #f5f5f5;
957 cursor: not-allowed;
958 }
959 `;
960
961 TM_addStyle(styles);
962 }
963
964 // Универсальная функция для поиска контейнера таблицы с вопросами
965 function findQuestionsTableContainer() {
966 console.log('Searching for questions table container...');
967
968 // Стратегия 1: Ищем таблицу по заголовку колонки "Вопрос"
969 const tables = document.querySelectorAll('table');
970 for (const table of tables) {
971 const headers = table.querySelectorAll('th');
972 for (const header of headers) {
973 if (header.textContent.trim() === 'Вопрос') {
974 console.log('Found table by "Вопрос" header');
975 // Ищем самый внешний контейнер с overflow или специфичным классом
976 let container = table;
977 let bestContainer = table.parentElement;
978
979 while (container.parentElement && container.parentElement.tagName !== 'BODY') {
980 container = container.parentElement;
981 const style = window.getComputedStyle(container);
982
983 // Ищем контейнер с overflow: auto или scroll
984 if (style.overflow === 'auto' || style.overflow === 'scroll' ||
985 style.overflowX === 'auto' || style.overflowX === 'scroll' ||
986 style.overflowY === 'auto' || style.overflowY === 'scroll') {
987 bestContainer = container;
988 }
989 }
990
991 console.log('Found container:', bestContainer.className);
992 // Возвращаем родительский контейнер, чтобы вставить панель ВНЕ Vue-контейнера
993 return bestContainer.parentElement || bestContainer;
994 }
995 }
996 }
997
998 // Стратегия 2: Ищем по data-widget атрибуту
999 const widget = document.querySelector('[data-widget="@seller-ui/reviews"]');
1000 if (widget) {
1001 const table = widget.querySelector('table');
1002 if (table) {
1003 console.log('Found table by data-widget');
1004 // Ищем ближайший контейнер с overflow
1005 let container = table.parentElement;
1006 while (container && container !== widget) {
1007 const style = window.getComputedStyle(container);
1008 if (style.overflow === 'auto' || style.overflow === 'scroll' ||
1009 style.overflowX === 'auto' || style.overflowX === 'scroll') {
1010 return container.parentElement || container;
1011 }
1012 container = container.parentElement;
1013 }
1014 return table.parentElement;
1015 }
1016 }
1017
1018 // Стратегия 3: Ищем таблицу с tbody, содержащим кнопки с вопросами
1019 for (const table of tables) {
1020 const questionButtons = table.querySelectorAll('tbody button');
1021 if (questionButtons.length > 0) {
1022 console.log('Found table by question buttons');
1023 // Ищем родительский контейнер с overflow
1024 let container = table.parentElement;
1025 while (container && container.tagName !== 'BODY') {
1026 const style = window.getComputedStyle(container);
1027 if (style.overflow === 'auto' || style.overflow === 'scroll' ||
1028 style.overflowX === 'auto' || style.overflowX === 'scroll') {
1029 return container.parentElement || container;
1030 }
1031 container = container.parentElement;
1032 }
1033 return table.parentElement;
1034 }
1035 }
1036
1037 console.error('Table container not found with any strategy');
1038 return null;
1039 }
1040
1041 // Функция для создания панели массовой генерации
1042 function createBulkGenerationPanel() {
1043 console.log('createBulkGenerationPanel called');
1044 const tableContainer = findQuestionsTableContainer();
1045 console.log('Table container found:', !!tableContainer);
1046 if (!tableContainer) {
1047 console.error('Table container not found');
1048 return;
1049 }
1050
1051 // Проверяем, не создана ли уже панель
1052 const existingPanel = document.querySelector('.bulk-generation-panel');
1053 console.log('Existing panel found:', !!existingPanel);
1054 if (existingPanel) {
1055 console.log('Bulk generation panel already exists');
1056 return;
1057 }
1058
1059 console.log('Creating bulk generation panel...');
1060 const panel = document.createElement('div');
1061 panel.className = 'bulk-generation-panel';
1062
1063 // Добавляем атрибут для изоляции от Vue
1064 panel.setAttribute('data-v-app', 'false');
1065
1066 panel.innerHTML = `
1067 <div class="bulk-generation-title">🤖 Массовая генерация ответов</div>
1068 <div class="bulk-generation-buttons">
1069 <button class="bulk-generate-btn">Сгенерировать ответы</button>
1070 <button class="bulk-answer-all-btn" disabled>Ответить всем</button>
1071 <button class="kb-upload-btn">📚 Загрузить базу знаний</button>
1072 <button class="prompt-edit-btn">✏️ Промпт</button>
1073 <button class="model-select-btn">⚙️ Модель</button>
1074 <input type="file" class="kb-file-input" accept=".xls,.xlsx" />
1075 </div>
1076 <div class="bulk-generation-status"></div>
1077 <div class="kb-status">База знаний не загружена</div>
1078 `;
1079
1080 console.log('Inserting panel before table container...');
1081 tableContainer.insertAdjacentElement('beforebegin', panel);
1082 console.log('Panel inserted');
1083
1084 // Добавляем обработчики
1085 const generateBtn = panel.querySelector('.bulk-generate-btn');
1086 const answerAllBtn = panel.querySelector('.bulk-answer-all-btn');
1087 const kbUploadBtn = panel.querySelector('.kb-upload-btn');
1088 const promptEditBtn = panel.querySelector('.prompt-edit-btn');
1089 const kbFileInput = panel.querySelector('.kb-file-input');
1090
1091 generateBtn.addEventListener('click', () => bulkGenerateAnswers());
1092 answerAllBtn.addEventListener('click', () => bulkAnswerAll());
1093
1094 kbUploadBtn.addEventListener('click', () => {
1095 kbFileInput.click();
1096 });
1097
1098 promptEditBtn.addEventListener('click', () => {
1099 showPromptModal();
1100 });
1101
1102 const modelSelectBtn = panel.querySelector('.model-select-btn');
1103
1104 modelSelectBtn.addEventListener('click', () => {
1105 showModelModal();
1106 });
1107
1108 kbFileInput.addEventListener('change', handleFileUpload);
1109
1110 console.log('Bulk generation panel created');
1111
1112 // Загружаем базу знаний из localStorage
1113 loadKnowledgeBase();
1114
1115 // Загружаем кастомный промпт из localStorage
1116 loadCustomPrompt();
1117 }
1118
1119 // Функция для получения всех видимых вопросов
1120 function getVisibleQuestions() {
1121 // Ищем все строки в tbody, которые содержат ссылку на товар и кнопку с вопросом
1122 const allRows = document.querySelectorAll('tbody tr');
1123 const questions = [];
1124
1125 allRows.forEach((row, index) => {
1126 // Ищем ссылку на товар в 3-й колонке
1127 const productLink = row.querySelector('td:nth-child(3) a');
1128 // Ищем кнопку с вопросом в 4-й колонке
1129 const questionButton = row.querySelector('td:nth-child(4) button');
1130
1131 // Ищем SKU - это div с классом body-400 в 3-й колонке
1132 const skuElements = row.querySelectorAll('td:nth-child(3) div');
1133 let sku = '';
1134 for (const el of skuElements) {
1135 const text = el.textContent.trim();
1136 // SKU обычно числовой и длиной 8-12 символов
1137 if (text.length >= 8 && text.length <= 12 && /^\d+$/.test(text)) {
1138 sku = text;
1139 break;
1140 }
1141 }
1142
1143 // Получаем количество ответов из 5-й колонки
1144 const answersCountElement = row.querySelector('td:nth-child(5)');
1145 const answersCount = answersCountElement ? parseInt(answersCountElement.textContent.trim()) : 0;
1146
1147 if (productLink && questionButton) {
1148 questions.push({
1149 index: index,
1150 row: row,
1151 productName: productLink.textContent.trim(),
1152 questionText: questionButton.textContent.trim(),
1153 questionButton: questionButton,
1154 sku: sku,
1155 answersCount: answersCount
1156 });
1157 }
1158 });
1159
1160 return questions;
1161 }
1162
1163 // Функция для массовой генерации ответов
1164 async function bulkGenerateAnswers() {
1165 const generateBtn = document.querySelector('.bulk-generate-btn');
1166 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1167 const statusDiv = document.querySelector('.bulk-generation-status');
1168
1169 try {
1170 generateBtn.disabled = true;
1171 answerAllBtn.disabled = true;
1172 generatedAnswers.clear();
1173
1174 const questions = getVisibleQuestions();
1175
1176 if (questions.length === 0) {
1177 statusDiv.textContent = 'Нет вопросов для обработки';
1178 return;
1179 }
1180
1181 statusDiv.textContent = `Генерация ответов: 0 из ${questions.length}`;
1182
1183 let skippedCount = 0;
1184 let alreadyAnsweredCount = 0;
1185
1186 for (let i = 0; i < questions.length; i++) {
1187 const question = questions[i];
1188
1189 try {
1190 // Пропускаем вопросы, на которые уже есть ответы
1191 if (question.answersCount > 0) {
1192 console.log(`Question ${i + 1} skipped: already has ${question.answersCount} answer(s)`);
1193 alreadyAnsweredCount++;
1194 continue;
1195 }
1196
1197 // Подсвечиваем текущий вопрос
1198 question.row.classList.add('question-row-processing');
1199
1200 statusDiv.textContent = `Генерация ответов: ${i + 1} из ${questions.length}`;
1201
1202 // Генерируем ответ
1203 const answer = await generateAnswerForQuestion(question.productName, question.questionText, question.sku);
1204
1205 // Проверяем валидность ответа
1206 if (!isValidAnswer(answer)) {
1207 console.log(`Question ${i + 1} skipped: AI returned invalid or SKIP answer`);
1208 question.row.classList.remove('question-row-processing');
1209 question.row.classList.add('question-row-error');
1210 skippedCount++;
1211 continue;
1212 }
1213
1214 // Сохраняем ответ
1215 generatedAnswers.set(i, answer);
1216
1217 // Добавляем textarea с ответом
1218 addAnswerTextarea(question.row, answer, i);
1219
1220 // Убираем подсветку обработки и добавляем подсветку завершения
1221 question.row.classList.remove('question-row-processing');
1222 question.row.classList.add('question-row-completed');
1223
1224 console.log(`Answer generated for question ${i + 1}:`, answer);
1225
1226 } catch (error) {
1227 console.error(`Error generating answer for question ${i + 1}:`, error);
1228 TM_log(`ERROR: Error generating answer for question ${i + 1}:`, error.message || error.toString());
1229 question.row.classList.remove('question-row-processing');
1230 question.row.classList.add('question-row-error');
1231 skippedCount++;
1232 // Пропускаем вопрос при ошибке
1233 }
1234 }
1235
1236 let statusText = `Генерация завершена: ${generatedAnswers.size} из ${questions.length} ответов`;
1237 if (alreadyAnsweredCount > 0) {
1238 statusText += ` (уже отвечено: ${alreadyAnsweredCount})`;
1239 }
1240 if (skippedCount > 0) {
1241 statusText += ` (пропущено: ${skippedCount})`;
1242 }
1243 statusDiv.textContent = statusText;
1244 answerAllBtn.disabled = generatedAnswers.size === 0;
1245
1246 } catch (error) {
1247 console.error('Bulk generation error:', error);
1248 statusDiv.textContent = 'Ошибка при массовой генерации';
1249 } finally {
1250 generateBtn.disabled = false;
1251 }
1252 }
1253
1254 // Функция для генерации ответа на один вопрос
1255 async function generateAnswerForQuestion(productName, questionText, sku) {
1256 // Ищем товар в базе знаний по SKU
1257 const productInfo = findProductInKnowledgeBase(sku);
1258 let knowledgeBaseInfo = '';
1259
1260 if (productInfo) {
1261 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
1262 console.log('Using knowledge base info for bulk generation');
1263 }
1264
1265 // Используем кастомный промпт или дефолтный
1266 const promptTemplate = getCurrentPrompt();
1267
1268 // Заменяем переменные в промпте
1269 const prompt = promptTemplate
1270 .replace('{productName}', productName)
1271 .replace('{questionText}', questionText)
1272 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo)
1273 .replace('{additionalPrompt}', '');
1274
1275 const answer = await callAI(prompt);
1276 return answer;
1277 }
1278
1279 // Функция для добавления textarea с ответом
1280 function addAnswerTextarea(row, answer, index) {
1281 // Проверяем, не добавлен ли уже textarea
1282 const existingContainer = row.querySelector('.answer-textarea-container');
1283 if (existingContainer) {
1284 const textarea = existingContainer.querySelector('.answer-textarea');
1285 textarea.value = answer;
1286 autoResizeTextarea(textarea);
1287 return;
1288 }
1289
1290 const questionCell = row.querySelector('td:nth-child(4)');
1291 if (!questionCell) return;
1292
1293 const container = document.createElement('div');
1294 container.className = 'answer-textarea-container';
1295
1296 container.innerHTML = `
1297 <div class="answer-label">Сгенерированный ответ:</div>
1298 <textarea class="answer-textarea" data-question-index="${index}">${answer}</textarea>
1299 `;
1300
1301 questionCell.appendChild(container);
1302
1303 // Обновляем значение в Map при редактировании
1304 const textarea = container.querySelector('.answer-textarea');
1305
1306 // Автоматически изменяем размер textarea
1307 autoResizeTextarea(textarea);
1308
1309 // Предотвращаем открытие модального окна при клике на textarea или контейнер
1310 container.addEventListener('click', (e) => {
1311 e.stopPropagation();
1312 });
1313
1314 textarea.addEventListener('click', (e) => {
1315 e.stopPropagation();
1316 });
1317
1318 textarea.addEventListener('input', () => {
1319 generatedAnswers.set(index, textarea.value);
1320 autoResizeTextarea(textarea);
1321 updateAnswerAllButton();
1322 });
1323
1324 // Активируем кнопку "Ответить всем"
1325 updateAnswerAllButton();
1326 }
1327
1328 // Функция для обновления состояния кнопки "Ответить всем"
1329 function updateAnswerAllButton() {
1330 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1331 if (!answerAllBtn) return;
1332
1333 // Проверяем есть ли хотя бы один ответ
1334 const questions = getVisibleQuestions();
1335 let hasAnswers = false;
1336
1337 for (const question of questions) {
1338 const answerTextarea = question.row.querySelector('.answer-textarea');
1339 if (answerTextarea && answerTextarea.value.trim()) {
1340 hasAnswers = true;
1341 break;
1342 }
1343 }
1344
1345 answerAllBtn.disabled = !hasAnswers;
1346 }
1347
1348 // Функция для автоматического изменения размера textarea
1349 function autoResizeTextarea(textarea) {
1350 // Сбрасываем высоту для правильного расчета
1351 textarea.style.height = 'auto';
1352
1353 // Устанавливаем высоту на основе scrollHeight
1354 const newHeight = Math.max(150, textarea.scrollHeight);
1355 textarea.style.height = newHeight + 'px';
1356
1357 // Также адаптируем ширину, если текст очень длинный
1358 const lineLength = textarea.value.split('\n').reduce((max, line) => Math.max(max, line.length), 0);
1359 if (lineLength > 100) {
1360 textarea.style.width = '100%';
1361 }
1362 }
1363
1364 // Функция для автоматической отправки всех ответов
1365 async function bulkAnswerAll() {
1366 TM_log('=== BULK ANSWER ALL STARTED ===');
1367 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1368 const statusDiv = document.querySelector('.bulk-generation-status');
1369
1370 TM_log('Button found:', !!answerAllBtn);
1371 TM_log('Status div found:', !!statusDiv);
1372
1373 try {
1374 answerAllBtn.disabled = true;
1375 TM_log('Button disabled');
1376
1377 const questions = getVisibleQuestions();
1378 TM_log('Questions found:', questions.length);
1379 let successCount = 0;
1380 let totalToSend = 0;
1381
1382 // Сначала подсчитываем сколько ответов нужно отправить
1383 for (let i = 0; i < questions.length; i++) {
1384 const question = questions[i];
1385 const answerTextareaOnPage = question.row.querySelector('.answer-textarea');
1386 if (answerTextareaOnPage && answerTextareaOnPage.value.trim()) {
1387 totalToSend++;
1388 TM_log(`Question ${i}: has answer, length: ${answerTextareaOnPage.value.trim().length}`);
1389 }
1390 }
1391
1392 TM_log('Total answers to send:', totalToSend);
1393
1394 if (totalToSend === 0) {
1395 statusDiv.textContent = 'Нет ответов для отправки';
1396 answerAllBtn.disabled = false;
1397 TM_log('No answers to send, exiting');
1398 return;
1399 }
1400
1401 statusDiv.textContent = `Отправка ответов: 0 из ${totalToSend}`;
1402
1403 for (let i = 0; i < questions.length; i++) {
1404 const question = questions[i];
1405
1406 // Получаем ответ из textarea на странице (не в модальном окне!)
1407 const answerTextareaOnPage = question.row.querySelector('.answer-textarea');
1408 if (!answerTextareaOnPage || !answerTextareaOnPage.value.trim()) {
1409 TM_log(`Question ${i + 1} skipped: no answer`);
1410 continue;
1411 }
1412
1413 const answer = answerTextareaOnPage.value.trim();
1414
1415 try {
1416 TM_log(`Processing question ${i + 1}, answer length:`, answer.length);
1417 statusDiv.textContent = `Отправка ответов: ${successCount + 1} из ${totalToSend}`;
1418
1419 // Кликаем на вопрос
1420 TM_log('About to click question button');
1421 question.questionButton.click();
1422 TM_log('Clicked on question button');
1423
1424 // Ждем открытия модального окна
1425 TM_log('Waiting for modal to open...');
1426 await waitForModal();
1427 TM_log('Modal opened');
1428
1429 // Находим модальное окно - ищем div с текстом "Ответ на вопрос"
1430 const answerSectionTitle = Array.from(document.querySelectorAll('*'))
1431 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1432
1433 if (!answerSectionTitle) {
1434 throw new Error('Answer section not found');
1435 }
1436 TM_log('Answer section title found');
1437
1438 // Находим родительский контейнер с классом mt7
1439 let answerContainer = answerSectionTitle.parentElement;
1440 if (!answerContainer || !answerContainer.classList.contains('mt7')) {
1441 throw new Error('Answer container not found');
1442 }
1443 TM_log('Answer container found');
1444
1445 // Находим textarea внутри этого контейнера - ищем по label "Ваш ответ"
1446 const label = Array.from(answerContainer.querySelectorAll('label'))
1447 .find(l => l.textContent.trim() === 'Ваш ответ');
1448
1449 if (!label) {
1450 throw new Error('Label "Ваш ответ" not found');
1451 }
1452
1453 // Получаем id из атрибута for
1454 const textareaId = label.getAttribute('for');
1455 if (!textareaId) {
1456 throw new Error('Textarea id not found in label');
1457 }
1458 TM_log('Textarea id from label:', textareaId);
1459
1460 // Находим textarea по id
1461 const textareaInModal = document.getElementById(textareaId);
1462 if (!textareaInModal) {
1463 throw new Error('Textarea not found by id: ' + textareaId);
1464 }
1465 TM_log('Textarea found in modal by id');
1466
1467 // Вставляем ответ, который мы взяли СО СТРАНИЦЫ
1468 textareaInModal.value = answer;
1469 TM_log('Answer set to modal textarea, length:', textareaInModal.value.length);
1470
1471 // Генерируем события для обновления состояния
1472 const inputEvent = new Event('input', { bubbles: true });
1473 textareaInModal.dispatchEvent(inputEvent);
1474
1475 const changeEvent = new Event('change', { bubbles: true });
1476 textareaInModal.dispatchEvent(changeEvent);
1477
1478 // Также пробуем установить фокус и снять его для активации валидации
1479 textareaInModal.focus();
1480 textareaInModal.blur();
1481
1482 TM_log('Events dispatched');
1483
1484 // Ждем немного для обновления UI
1485 await sleep(1000);
1486
1487 // Находим кнопку "Отправить ответ" - ищем по тексту внутри кнопки
1488 const submitButton = Array.from(answerContainer.querySelectorAll('button[type="submit"]'))
1489 .find(btn => btn.textContent.includes('Отправить ответ'));
1490
1491 if (!submitButton) {
1492 TM_log('Submit button not found, available buttons:',
1493 Array.from(answerContainer.querySelectorAll('button')).map(b => b.textContent));
1494 throw new Error('Submit button not found');
1495 }
1496 TM_log('Submit button found, text:', submitButton.textContent);
1497 TM_log('Submit button disabled:', submitButton.disabled);
1498
1499 // Если кнопка все еще disabled, пробуем еще раз обновить textarea
1500 if (submitButton.disabled) {
1501 TM_log('Button still disabled, trying to re-trigger events...');
1502 textareaInModal.value = answer;
1503 textareaInModal.dispatchEvent(new Event('input', { bubbles: true }));
1504 textareaInModal.dispatchEvent(new Event('change', { bubbles: true }));
1505 await sleep(500);
1506 TM_log('Submit button disabled after retry:', submitButton.disabled);
1507 }
1508
1509 submitButton.click();
1510 TM_log('Submit button clicked');
1511
1512 // Ждем немного после отправки
1513 await sleep(1500);
1514
1515 // Закрываем модальное окно - ищем кнопку с aria-label "Крестик для закрытия"
1516 const closeButton = Array.from(document.querySelectorAll('button'))
1517 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1518
1519 if (closeButton) {
1520 closeButton.click();
1521 TM_log('Close button clicked');
1522 } else {
1523 TM_log('Close button not found, trying ESC key');
1524 // Альтернативный способ - нажатие ESC
1525 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
1526 }
1527
1528 // Ждем закрытия модального окна
1529 await waitForModalClose();
1530 TM_log('Modal closed');
1531
1532 successCount++;
1533 TM_log(`Answer ${successCount} sent successfully`);
1534
1535 } catch (error) {
1536 TM_log(`ERROR: Error sending answer for question ${i + 1}:`, error.message);
1537 console.error('Full error:', error);
1538 // Закрываем модальное окно при ошибке
1539 const closeButton = Array.from(document.querySelectorAll('button'))
1540 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1541 if (closeButton) {
1542 closeButton.click();
1543 } else {
1544 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
1545 }
1546 await sleep(500);
1547 }
1548 }
1549
1550 statusDiv.textContent = `Отправка завершена: ${successCount} из ${totalToSend} ответов отправлено`;
1551 TM_log('=== BULK ANSWER ALL COMPLETED ===');
1552
1553 } catch (error) {
1554 TM_log('ERROR: Bulk answer error:', error.message);
1555 console.error('Full bulk answer error:', error);
1556 statusDiv.textContent = 'Ошибка при отправке ответов';
1557 } finally {
1558 answerAllBtn.disabled = false;
1559 TM_log('Button re-enabled');
1560 }
1561 }
1562
1563 // Вспомогательная функция ожидания модального окна
1564 function waitForModal() {
1565 return new Promise((resolve) => {
1566 const checkModal = () => {
1567 // Ищем div с текстом "Ответ на вопрос"
1568 const answerSection = Array.from(document.querySelectorAll('*'))
1569 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1570
1571 if (answerSection) {
1572 // Проверяем, что есть textarea с label "Ваш ответ"
1573 const container = answerSection.parentElement;
1574 if (container) {
1575 const label = Array.from(container.querySelectorAll('label'))
1576 .find(l => l.textContent.trim() === 'Ваш ответ');
1577 if (label) {
1578 resolve();
1579 return;
1580 }
1581 }
1582 }
1583 setTimeout(checkModal, 100);
1584 };
1585 checkModal();
1586 });
1587 }
1588
1589 // Вспомогательная функция ожидания закрытия модального окна
1590 function waitForModalClose() {
1591 return new Promise((resolve) => {
1592 const checkModal = () => {
1593 // Проверяем, что модальное окно с вопросом закрыто
1594 const answerSection = Array.from(document.querySelectorAll('*'))
1595 .find(el => el.textContent.trim() === 'Ответ на вопрос' && el.tagName === 'DIV');
1596
1597 if (!answerSection) {
1598 resolve();
1599 return;
1600 }
1601
1602 // Проверяем, что есть кнопка закрытия
1603 const closeButton = Array.from(document.querySelectorAll('button'))
1604 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия');
1605
1606 if (closeButton) {
1607 resolve();
1608 return;
1609 }
1610
1611 setTimeout(checkModal, 100);
1612 };
1613 checkModal();
1614 });
1615 }
1616
1617 // Вспомогательная функция задержки
1618 function sleep(ms) {
1619 return new Promise(resolve => setTimeout(resolve, ms));
1620 }
1621
1622 // Функция для загрузки промпта из localStorage
1623 async function loadCustomPrompt() {
1624 try {
1625 const stored = localStorage.getItem('ozon_custom_prompt');
1626 if (stored) {
1627 customPrompt = stored;
1628 console.log('Custom prompt loaded');
1629 }
1630 } catch (error) {
1631 console.error('Error loading custom prompt:', error);
1632 }
1633 }
1634
1635 // Функция для загрузки промпта для отзывов из localStorage
1636 async function loadReviewsPrompt() {
1637 try {
1638 const stored = localStorage.getItem('ozon_reviews_prompt');
1639 if (stored) {
1640 reviewsPrompt = stored;
1641 console.log('Reviews prompt loaded');
1642 }
1643 } catch (error) {
1644 console.error('Error loading reviews prompt:', error);
1645 }
1646 }
1647
1648 // Функция для сохранения промпта в localStorage
1649 function saveCustomPrompt(prompt) {
1650 try {
1651 localStorage.setItem('ozon_custom_prompt', prompt);
1652 customPrompt = prompt;
1653 console.log('Custom prompt saved');
1654 } catch (error) {
1655 console.error('Error saving custom prompt:', error);
1656 alert('Ошибка при сохранении промпта: ' + error.message);
1657 }
1658 }
1659
1660 // Функция для сохранения промпта для отзывов в localStorage
1661 function saveReviewsPrompt(prompt) {
1662 try {
1663 localStorage.setItem('ozon_reviews_prompt', prompt);
1664 reviewsPrompt = prompt;
1665 console.log('Reviews prompt saved');
1666 } catch (error) {
1667 console.error('Error saving reviews prompt:', error);
1668 alert('Ошибка при сохранении промпта для отзывов: ' + error.message);
1669 }
1670 }
1671
1672 // Функция для получения текущего промпта
1673 function getCurrentPrompt() {
1674 return customPrompt || DEFAULT_PROMPT;
1675 }
1676
1677 // Функция для получения текущего промпта для отзывов
1678 function getCurrentReviewsPrompt() {
1679 return reviewsPrompt || DEFAULT_REVIEWS_PROMPT;
1680 }
1681
1682 // Функция для показа модального окна с промптом
1683 function showPromptModal() {
1684 // Проверяем, не открыто ли уже модальное окно
1685 if (document.querySelector('.prompt-modal-overlay')) {
1686 return;
1687 }
1688
1689 const overlay = document.createElement('div');
1690 overlay.className = 'prompt-modal-overlay';
1691
1692 const modal = document.createElement('div');
1693 modal.className = 'prompt-modal';
1694
1695 const currentPrompt = getCurrentPrompt();
1696
1697 modal.innerHTML = `
1698 <div class="prompt-modal-header">
1699 <h3 class="prompt-modal-title">Редактирование промпта</h3>
1700 <button class="prompt-modal-close">✕</button>
1701 </div>
1702 <div class="prompt-modal-body">
1703 <div class="prompt-modal-hint">
1704 Используйте переменные: {productName}, {questionText}, {knowledgeBaseInfo}
1705 </div>
1706 <textarea class="prompt-modal-textarea">${currentPrompt}</textarea>
1707 </div>
1708 <div class="prompt-modal-footer">
1709 <button class="prompt-modal-reset">Сбросить на дефолтный</button>
1710 <div class="prompt-modal-actions">
1711 <button class="prompt-modal-cancel">Отмена</button>
1712 <button class="prompt-modal-save">Сохранить</button>
1713 </div>
1714 </div>
1715 `;
1716
1717 overlay.appendChild(modal);
1718 document.body.appendChild(overlay);
1719
1720 // Обработчики
1721 const closeBtn = modal.querySelector('.prompt-modal-close');
1722 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1723 const saveBtn = modal.querySelector('.prompt-modal-save');
1724 const resetBtn = modal.querySelector('.prompt-modal-reset');
1725 const textarea = modal.querySelector('.prompt-modal-textarea');
1726
1727 const closeModal = () => {
1728 overlay.remove();
1729 };
1730
1731 closeBtn.addEventListener('click', closeModal);
1732 cancelBtn.addEventListener('click', closeModal);
1733 overlay.addEventListener('click', (e) => {
1734 if (e.target === overlay) closeModal();
1735 });
1736
1737 saveBtn.addEventListener('click', () => {
1738 const newPrompt = textarea.value.trim();
1739 if (newPrompt) {
1740 saveCustomPrompt(newPrompt);
1741 alert('Промпт сохранен успешно!');
1742 closeModal();
1743 } else {
1744 alert('Промпт не может быть пустым');
1745 }
1746 });
1747
1748 resetBtn.addEventListener('click', () => {
1749 if (confirm('Вы уверены, что хотите сбросить промпт на дефолтный?')) {
1750 textarea.value = DEFAULT_PROMPT;
1751 localStorage.removeItem('ozon_custom_prompt');
1752 customPrompt = null;
1753 alert('Промпт сброшен на дефолтный');
1754 }
1755 });
1756 }
1757
1758 // Функция для показа модального окна с промптом для отзывов
1759 function showReviewsPromptModal() {
1760 // Проверяем, не открыто ли уже модальное окно
1761 if (document.querySelector('.prompt-modal-overlay')) {
1762 return;
1763 }
1764
1765 const overlay = document.createElement('div');
1766 overlay.className = 'prompt-modal-overlay';
1767
1768 const modal = document.createElement('div');
1769 modal.className = 'prompt-modal';
1770
1771 const currentPrompt = getCurrentReviewsPrompt();
1772
1773 modal.innerHTML = `
1774 <div class="prompt-modal-header">
1775 <h3 class="prompt-modal-title">Редактирование промпта для отзывов</h3>
1776 <button class="prompt-modal-close">✕</button>
1777 </div>
1778 <div class="prompt-modal-body">
1779 <div class="prompt-modal-hint">
1780 Используйте переменные: {productName}, {rating}, {comment}, {knowledgeBaseInfo}
1781 </div>
1782 <textarea class="prompt-modal-textarea">${currentPrompt}</textarea>
1783 </div>
1784 <div class="prompt-modal-footer">
1785 <button class="prompt-modal-reset">Сбросить на дефолтный</button>
1786 <div class="prompt-modal-actions">
1787 <button class="prompt-modal-cancel">Отмена</button>
1788 <button class="prompt-modal-save">Сохранить</button>
1789 </div>
1790 </div>
1791 `;
1792
1793 overlay.appendChild(modal);
1794 document.body.appendChild(overlay);
1795
1796 // Обработчики
1797 const closeBtn = modal.querySelector('.prompt-modal-close');
1798 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1799 const saveBtn = modal.querySelector('.prompt-modal-save');
1800 const resetBtn = modal.querySelector('.prompt-modal-reset');
1801 const textarea = modal.querySelector('.prompt-modal-textarea');
1802
1803 const closeModal = () => {
1804 overlay.remove();
1805 };
1806
1807 closeBtn.addEventListener('click', closeModal);
1808 cancelBtn.addEventListener('click', closeModal);
1809 overlay.addEventListener('click', (e) => {
1810 if (e.target === overlay) closeModal();
1811 });
1812
1813 saveBtn.addEventListener('click', () => {
1814 const newPrompt = textarea.value.trim();
1815 if (newPrompt) {
1816 saveReviewsPrompt(newPrompt);
1817 alert('Промпт для отзывов сохранен успешно!');
1818 closeModal();
1819 } else {
1820 alert('Промпт не может быть пустым');
1821 }
1822 });
1823
1824 resetBtn.addEventListener('click', () => {
1825 if (confirm('Вы уверены, что хотите сбросить промпт на дефолтный?')) {
1826 textarea.value = DEFAULT_REVIEWS_PROMPT;
1827 localStorage.removeItem('ozon_reviews_prompt');
1828 reviewsPrompt = null;
1829 alert('Промпт сброшен на дефолтный');
1830 }
1831 });
1832 }
1833
1834 // Функция для показа модального окна выбора модели
1835 function showModelModal() {
1836 // Проверяем, не открыто ли уже модальное окно
1837 if (document.querySelector('.prompt-modal-overlay')) {
1838 return;
1839 }
1840
1841 // Загружаем настройки модели из localStorage
1842 loadModelSettings();
1843
1844 const overlay = document.createElement('div');
1845 overlay.className = 'prompt-modal-overlay';
1846
1847 const modal = document.createElement('div');
1848 modal.className = 'prompt-modal';
1849
1850 modal.innerHTML = `
1851 <div class="prompt-modal-header">
1852 <h3 class="prompt-modal-title">Настройки AI модели</h3>
1853 <button class="prompt-modal-close">✕</button>
1854 </div>
1855 <div class="prompt-modal-body">
1856 <div class="model-modal-form-group">
1857 <label class="model-modal-label">Провайдер:</label>
1858 <select class="model-modal-select" id="model-provider-select">
1859 <option value="rmcall" ${modelSettings.provider === 'rmcall' ? 'selected' : ''}>RM Call (стандартный)</option>
1860 <option value="openrouter" ${modelSettings.provider === 'openrouter' ? 'selected' : ''}>OpenRouter</option>
1861 </select>
1862 </div>
1863
1864 <div class="model-modal-form-group" id="model-select-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1865 <label class="model-modal-label">
1866 Модель:
1867 <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;">
1868 🔍 Проверить модели
1869 </button>
1870 </label>
1871 <select class="model-modal-select" id="model-name-select" size="8" style="height: auto;">
1872 ${getModelOptions()}
1873 </select>
1874 <div id="model-test-status" style="margin-top: 8px; font-size: 12px; color: #666;"></div>
1875 <div style="margin-top: 12px; display: flex; gap: 8px;">
1876 <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;" />
1877 <button id="add-model-btn" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">➕ Добавить</button>
1878 <button id="remove-model-btn" style="padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">🗑️ Удалить</button>
1879 </div>
1880 </div>
1881
1882 <div class="model-modal-form-group" id="api-key-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1883 <label class="model-modal-label">API ключ OpenRouter:</label>
1884 <input type="password" class="model-modal-input" id="api-key-input" placeholder="Введите API ключ" value="${modelSettings.apiKey || ''}" />
1885 <div class="prompt-modal-hint" style="margin-top: 8px;">
1886 Получите бесплатный API ключ на <a href="https://openrouter.ai/keys" target="_blank" style="color: #17a2b8;">openrouter.ai/keys</a>
1887 </div>
1888 </div>
1889 </div>
1890 <div class="prompt-modal-footer">
1891 <div></div>
1892 <div class="prompt-modal-actions">
1893 <button class="prompt-modal-cancel">Отмена</button>
1894 <button class="prompt-modal-save">Сохранить</button>
1895 </div>
1896 </div>
1897 `;
1898
1899 overlay.appendChild(modal);
1900 document.body.appendChild(overlay);
1901
1902 // Обработчики
1903 const closeBtn = modal.querySelector('.prompt-modal-close');
1904 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1905 const saveBtn = modal.querySelector('.prompt-modal-save');
1906 const providerSelect = modal.querySelector('#model-provider-select');
1907 const modelNameSelect = modal.querySelector('#model-name-select');
1908 const apiKeyInput = modal.querySelector('#api-key-input');
1909 const modelSelectGroup = modal.querySelector('#model-select-group');
1910 const apiKeyGroup = modal.querySelector('#api-key-group');
1911 const testModelsBtn = modal.querySelector('#test-models-btn');
1912
1913 // Устанавливаем текущую модель
1914 if (modelSettings.model) {
1915 modelNameSelect.value = modelSettings.model;
1916 }
1917
1918 const closeModal = () => {
1919 overlay.remove();
1920 };
1921
1922 // Обработчик изменения провайдера
1923 providerSelect.addEventListener('change', () => {
1924 const isOpenRouter = providerSelect.value === 'openrouter';
1925 modelSelectGroup.style.display = isOpenRouter ? 'block' : 'none';
1926 apiKeyGroup.style.display = isOpenRouter ? 'block' : 'none';
1927 });
1928
1929 // Обработчик проверки моделей
1930 testModelsBtn.addEventListener('click', async () => {
1931 await testAllModels(modelNameSelect, apiKeyInput.value.trim());
1932 });
1933
1934 // Обработчик добавления модели
1935 const addModelBtn = modal.querySelector('#add-model-btn');
1936 const removeModelBtn = modal.querySelector('#remove-model-btn');
1937 const customModelInput = modal.querySelector('#custom-model-input');
1938
1939 addModelBtn.addEventListener('click', () => {
1940 const modelId = customModelInput.value.trim();
1941 if (!modelId) {
1942 alert('Пожалуйста, введите ID модели');
1943 return;
1944 }
1945
1946 // Проверяем, не добавлена ли уже эта модель
1947 if (!modelSettings.customModels) {
1948 modelSettings.customModels = [];
1949 }
1950
1951 if (modelSettings.customModels.includes(modelId)) {
1952 alert('Эта модель уже добавлена');
1953 return;
1954 }
1955
1956 // Добавляем модель
1957 modelSettings.customModels.push(modelId);
1958 saveModelSettings();
1959
1960 // Обновляем список моделей
1961 modelNameSelect.innerHTML = getModelOptions();
1962 modelNameSelect.value = modelId;
1963
1964 customModelInput.value = '';
1965 alert('Модель добавлена успешно!');
1966 });
1967
1968 removeModelBtn.addEventListener('click', () => {
1969 const selectedModel = modelNameSelect.value;
1970 if (!selectedModel) {
1971 alert('Пожалуйста, выберите модель для удаления');
1972 return;
1973 }
1974
1975 // Проверяем, является ли модель кастомной
1976 if (!modelSettings.customModels || !modelSettings.customModels.includes(selectedModel)) {
1977 alert('Можно удалять только добавленные вами модели');
1978 return;
1979 }
1980
1981 if (!confirm(`Вы уверены, что хотите удалить модель "${selectedModel}"?`)) {
1982 return;
1983 }
1984
1985 // Удаляем модель
1986 modelSettings.customModels = modelSettings.customModels.filter(m => m !== selectedModel);
1987 saveModelSettings();
1988
1989 // Обновляем список моделей
1990 modelNameSelect.innerHTML = getModelOptions();
1991
1992 alert('Модель удалена успешно!');
1993 });
1994
1995 closeBtn.addEventListener('click', closeModal);
1996 cancelBtn.addEventListener('click', closeModal);
1997 overlay.addEventListener('click', (e) => {
1998 if (e.target === overlay) closeModal();
1999 });
2000
2001 saveBtn.addEventListener('click', () => {
2002 const provider = providerSelect.value;
2003 const model = modelNameSelect.value;
2004 const apiKey = apiKeyInput.value.trim();
2005
2006 // Валидация
2007 if (provider === 'openrouter' && !apiKey) {
2008 alert('Пожалуйста, введите API ключ для OpenRouter');
2009 return;
2010 }
2011
2012 // Сохраняем настройки
2013 modelSettings.provider = provider;
2014 modelSettings.model = model;
2015 modelSettings.apiKey = apiKey;
2016
2017 saveModelSettings();
2018 alert('Настройки модели сохранены успешно!');
2019 closeModal();
2020 });
2021 }
2022
2023 // Функция для тестирования всех моделей
2024 async function testAllModels(selectElement, apiKey) {
2025 if (!apiKey) {
2026 alert('Пожалуйста, введите API ключ для проверки моделей');
2027 return;
2028 }
2029
2030 const statusDiv = document.querySelector('#model-test-status');
2031 const testBtn = document.querySelector('#test-models-btn');
2032
2033 testBtn.disabled = true;
2034 testBtn.textContent = '⏳ Проверка...';
2035 statusDiv.textContent = 'Проверка моделей...';
2036
2037 const models = [];
2038 for (let i = 0; i < selectElement.options.length; i++) {
2039 models.push({
2040 value: selectElement.options[i].value,
2041 text: selectElement.options[i].text,
2042 option: selectElement.options[i]
2043 });
2044 }
2045
2046 let workingCount = 0;
2047 let failedCount = 0;
2048
2049 for (let i = 0; i < models.length; i++) {
2050 const model = models[i];
2051 statusDiv.textContent = `Проверка ${i + 1} из ${models.length}: ${model.text}`;
2052
2053 try {
2054 const isWorking = await testModel(model.value, apiKey);
2055
2056 if (isWorking) {
2057 model.option.text = '✅ ' + model.text.replace('✅ ', '').replace('❌ ', '');
2058 workingCount++;
2059 } else {
2060 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
2061 failedCount++;
2062 }
2063 } catch (error) {
2064 console.error(`Model ${model.value} test failed:`, error);
2065 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
2066 failedCount++;
2067 }
2068
2069 // Небольшая задержка между запросами
2070 await sleep(500);
2071 }
2072
2073 testBtn.disabled = false;
2074 testBtn.textContent = '🔍 Проверить модели';
2075 statusDiv.innerHTML = `<span style="color: #28a745;">✅ Работает: ${workingCount}</span> | <span style="color: #dc3545;">❌ Не работает: ${failedCount}</span>`;
2076 }
2077
2078 // Функция для тестирования одной модели
2079 async function testModel(modelName, apiKey) {
2080 try {
2081 const response = await GM.xmlhttpRequest({
2082 method: 'POST',
2083 url: 'https://openrouter.ai/api/v1/chat/completions',
2084 headers: {
2085 'Content-Type': 'application/json',
2086 'Authorization': `Bearer ${apiKey}`,
2087 'HTTP-Referer': window.location.href,
2088 'X-Title': 'Ozon AI Answer Generator'
2089 },
2090 data: JSON.stringify({
2091 model: modelName,
2092 messages: [
2093 {
2094 role: 'user',
2095 content: 'Ответь одним словом: привет'
2096 }
2097 ],
2098 max_tokens: 10
2099 }),
2100 timeout: 10000
2101 });
2102
2103 if (response.status === 200) {
2104 const data = JSON.parse(response.responseText);
2105 return !!(data.choices && data.choices[0] && data.choices[0].message);
2106 }
2107
2108 return false;
2109 } catch (error) {
2110 console.error(`Model ${modelName} test failed:`, error);
2111 return false;
2112 }
2113 }
2114
2115 // Функция для загрузки настроек модели из localStorage
2116 function loadModelSettings() {
2117 try {
2118 const stored = localStorage.getItem('ozon_model_settings');
2119 if (stored) {
2120 const settings = JSON.parse(stored);
2121 modelSettings.provider = settings.provider || 'rmcall';
2122 modelSettings.model = settings.model || 'google/gemini-2.0-flash-exp:free';
2123 modelSettings.apiKey = settings.apiKey || '';
2124 modelSettings.customModels = settings.customModels || [];
2125 console.log('Model settings loaded:', modelSettings);
2126 }
2127 } catch (error) {
2128 console.error('Error loading model settings:', error);
2129 }
2130 }
2131
2132 // Функция для генерации списка моделей
2133 function getModelOptions() {
2134 const defaultModels = [
2135 { value: 'arcee-ai/trinity-large-preview:free', label: 'Arcee Trinity Large (бесплатно)' },
2136 { value: 'openrouter/free', label: 'OpenRouter Free' }
2137 ];
2138
2139 let options = '';
2140
2141 // Добавляем дефолтные модели
2142 for (const model of defaultModels) {
2143 options += `<option value="${model.value}">${model.label}</option>`;
2144 }
2145
2146 // Добавляем кастомные модели
2147 if (modelSettings.customModels && modelSettings.customModels.length > 0) {
2148 options += '<option disabled>──────────</option>';
2149 for (const modelId of modelSettings.customModels) {
2150 options += `<option value="${modelId}">🔧 ${modelId}</option>`;
2151 }
2152 }
2153
2154 return options;
2155 }
2156
2157 // Функция для сохранения настроек модели в localStorage
2158 function saveModelSettings() {
2159 try {
2160 localStorage.setItem('ozon_model_settings', JSON.stringify(modelSettings));
2161 console.log('Model settings saved:', modelSettings);
2162 } catch (error) {
2163 console.error('Error saving model settings:', error);
2164 alert('Ошибка при сохранении настроек модели: ' + error.message);
2165 }
2166 }
2167
2168 // Универсальная функция для вызова AI
2169 async function callAI(prompt) {
2170 // Загружаем настройки модели
2171 loadModelSettings();
2172
2173 console.log('Using AI provider:', modelSettings.provider);
2174 console.log('Prompt being sent to AI:', prompt);
2175
2176 if (modelSettings.provider === 'openrouter') {
2177 // Используем OpenRouter API
2178 if (!modelSettings.apiKey) {
2179 throw new Error('API ключ OpenRouter не настроен. Откройте настройки модели и введите API ключ.');
2180 }
2181
2182 console.log('Calling OpenRouter API with model:', modelSettings.model);
2183
2184 try {
2185 const response = await GM.xmlhttpRequest({
2186 method: 'POST',
2187 url: 'https://openrouter.ai/api/v1/chat/completions',
2188 headers: {
2189 'Content-Type': 'application/json',
2190 'Authorization': `Bearer ${modelSettings.apiKey}`,
2191 'HTTP-Referer': window.location.href,
2192 'X-Title': 'Ozon AI Answer Generator'
2193 },
2194 data: JSON.stringify({
2195 model: modelSettings.model,
2196 messages: [
2197 {
2198 role: 'user',
2199 content: prompt
2200 }
2201 ]
2202 })
2203 });
2204
2205 console.log('OpenRouter response status:', response.status);
2206 console.log('OpenRouter response:', response.responseText);
2207
2208 if (response.status !== 200) {
2209 console.error('OpenRouter API error:', response);
2210 throw new Error(`OpenRouter API error: ${response.status} - ${response.statusText}`);
2211 }
2212
2213 const data = JSON.parse(response.responseText);
2214
2215 if (!data.choices || !data.choices[0] || !data.choices[0].message) {
2216 console.error('Invalid OpenRouter response:', data);
2217 throw new Error('Неверный формат ответа от OpenRouter API');
2218 }
2219
2220 return data.choices[0].message.content;
2221
2222 } catch (error) {
2223 console.error('OpenRouter API call failed:', error);
2224 throw new Error(`Ошибка вызова OpenRouter API: ${error.message}`);
2225 }
2226 } else {
2227 // Используем стандартный RM.aiCall
2228 console.log('Calling RM.aiCall');
2229 return await RM.aiCall(prompt);
2230 }
2231 }
2232
2233 // Инициализация массовой генерации
2234 function initBulkGeneration() {
2235 // Проверяем, что мы на странице со списком вопросов
2236 if (!window.location.href.includes('/app/reviews/questions')) {
2237 return;
2238 }
2239
2240 console.log('Initializing bulk generation...');
2241
2242 addBulkGenerationStyles();
2243
2244 // Ждем загрузки таблицы
2245 const observer = new MutationObserver(debounce(() => {
2246 const tableContainer = findQuestionsTableContainer();
2247 const existingPanel = document.querySelector('.bulk-generation-panel');
2248
2249 if (tableContainer) {
2250 // Если панель существует, но находится не перед контейнером таблицы
2251 if (existingPanel) {
2252 // Проверяем, находится ли панель перед текущим контейнером таблицы
2253 const panelNextSibling = existingPanel.nextElementSibling;
2254 if (panelNextSibling !== tableContainer) {
2255 console.log('Panel found but in wrong position, moving it...');
2256 tableContainer.insertAdjacentElement('beforebegin', existingPanel);
2257 console.log('Panel moved to correct position');
2258 }
2259 } else {
2260 // Панели нет, создаем новую
2261 console.log('Panel not found, creating new one...');
2262 createBulkGenerationPanel();
2263 }
2264 }
2265 }, 300));
2266
2267 observer.observe(document.body, {
2268 childList: true,
2269 subtree: true
2270 });
2271
2272 // Пробуем создать панель сразу
2273 const tableContainer = findQuestionsTableContainer();
2274 if (tableContainer) {
2275 createBulkGenerationPanel();
2276 }
2277 }
2278
2279 // Запускаем инициализацию массовой генерации
2280 initBulkGeneration();
2281
2282 // ============= МАССОВАЯ ГЕНЕРАЦИЯ ОТВЕТОВ НА ОТЗЫВЫ =============
2283
2284 // Состояние процесса обработки отзывов
2285 let reviewsProcessState = {
2286 isRunning: false,
2287 shouldStop: false,
2288 processedCount: 0,
2289 currentBatch: 0
2290 };
2291
2292 // Функция для добавления стилей для отзывов
2293 function addReviewsGenerationStyles() {
2294 const styles = `
2295 .reviews-generation-panel {
2296 margin: 20px 0;
2297 padding: 20px;
2298 background: #ffffff;
2299 border-radius: 8px;
2300 border: 2px solid #28a745;
2301 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
2302 }
2303
2304 .reviews-generation-title {
2305 font-size: 16px;
2306 font-weight: 600;
2307 margin-bottom: 16px;
2308 color: #1a1a1a;
2309 }
2310
2311 .reviews-generation-buttons {
2312 display: flex;
2313 gap: 12px;
2314 margin-bottom: 12px;
2315 flex-wrap: wrap;
2316 }
2317
2318 .reviews-generate-btn, .reviews-stop-btn {
2319 background: #28a745;
2320 color: white;
2321 border: none;
2322 padding: 12px 24px;
2323 border-radius: 6px;
2324 font-size: 14px;
2325 font-weight: 500;
2326 cursor: pointer;
2327 transition: background 0.2s;
2328 }
2329
2330 .reviews-generate-btn:hover {
2331 background: #218838;
2332 }
2333
2334 .reviews-stop-btn {
2335 background: #dc3545;
2336 }
2337
2338 .reviews-stop-btn:hover {
2339 background: #c82333;
2340 }
2341
2342 .reviews-generate-btn:disabled, .reviews-stop-btn:disabled {
2343 background: #d1d1d6;
2344 cursor: not-allowed;
2345 }
2346
2347 .reviews-prompt-edit-btn {
2348 background: #fd7e14;
2349 color: white;
2350 border: none;
2351 padding: 12px 24px;
2352 border-radius: 6px;
2353 font-size: 14px;
2354 font-weight: 500;
2355 cursor: pointer;
2356 transition: background 0.2s;
2357 }
2358
2359 .reviews-prompt-edit-btn:hover {
2360 background: #e8590c;
2361 }
2362
2363 .reviews-generation-status {
2364 font-size: 14px;
2365 color: #666;
2366 margin-top: 12px;
2367 }
2368
2369 .review-row-processing {
2370 background-color: #fff3cd !important;
2371 }
2372
2373 .review-row-completed {
2374 background-color: #d4edda !important;
2375 }
2376
2377 .review-row-error {
2378 background-color: #f8d7da !important;
2379 }
2380 `;
2381
2382 TM_addStyle(styles);
2383 }
2384
2385 // Функция для поиска контейнера таблицы с отзывами
2386 function findReviewsTableContainer() {
2387 console.log('Searching for reviews table container...');
2388
2389 // Ищем таблицу по заголовку колонки
2390 const tables = document.querySelectorAll('table');
2391 for (const table of tables) {
2392 const headers = table.querySelectorAll('th');
2393 for (const header of headers) {
2394 const headerText = header.textContent.trim();
2395 // Проверяем наличие характерных заголовков для страницы отзывов
2396 if (headerText === 'Товар' || headerText === 'Комментарий' || headerText === 'Оценка') {
2397 console.log('Found reviews table by header:', headerText);
2398 let container = table;
2399 let bestContainer = table.parentElement;
2400
2401 while (container.parentElement && container.parentElement.tagName !== 'BODY') {
2402 container = container.parentElement;
2403 const style = window.getComputedStyle(container);
2404
2405 if ((style.overflow === 'auto' || style.overflowX === 'auto') ||
2406 (container.className && container.className.includes('n1d-'))) {
2407 bestContainer = container;
2408 }
2409 }
2410
2411 console.log('Found container:', bestContainer.className);
2412 return bestContainer;
2413 }
2414 }
2415 }
2416
2417 console.error('Reviews table container not found');
2418 return null;
2419 }
2420
2421 // Функция для получения всех видимых отзывов
2422 function getVisibleReviews() {
2423 const allRows = document.querySelectorAll('tbody tr');
2424 const reviews = [];
2425
2426 allRows.forEach((row, index) => {
2427 // Получаем все ячейки
2428 const cells = row.querySelectorAll('td');
2429
2430 if (cells.length < 10) return;
2431
2432 // Столбец 3 (индекс 2) - товар
2433 const productCell = cells[2];
2434 const productLink = productCell ? productCell.querySelector('a') : null;
2435 const productName = productLink ? productLink.textContent.trim() : '';
2436
2437 // Ищем SKU в 3-й колонке
2438 const skuElements = productCell ? productCell.querySelectorAll('div') : [];
2439 let sku = '';
2440 for (const el of skuElements) {
2441 const text = el.textContent.trim();
2442 if (text.length >= 5 && text.length <= 12 && /^\d+$/.test(text)) {
2443 sku = text;
2444 break;
2445 }
2446 }
2447
2448 // Столбец 5 (индекс 4) - комментарий
2449 const commentCell = cells[4];
2450 const comment = commentCell ? commentCell.textContent.trim() : '';
2451
2452 // Столбец 7 (индекс 6) - рейтинг (звезды)
2453 const ratingCell = cells[6];
2454 const rating = ratingCell ? parseInt(ratingCell.textContent.trim()) : 0;
2455
2456 // Столбец 10 (индекс 9) - количество ответов
2457 const answersCell = cells[9];
2458 const answersCount = answersCell ? parseInt(answersCell.textContent.trim()) : 0;
2459
2460 if (productName) {
2461 reviews.push({
2462 index: index,
2463 row: row,
2464 productName: productName,
2465 comment: comment,
2466 rating: rating,
2467 sku: sku,
2468 answersCount: answersCount
2469 });
2470 }
2471 });
2472
2473 return reviews;
2474 }
2475
2476 // Функция для старта генерации ответов на отзывы
2477 async function startReviewsGeneration() {
2478 TM_log('startReviewsGeneration called');
2479 const generateBtn = document.querySelector('.reviews-generate-btn');
2480 const stopBtn = document.querySelector('.reviews-stop-btn');
2481 const statusDiv = document.querySelector('.reviews-generation-status');
2482
2483 // Если процесс уже запущен - игнорируем
2484 if (reviewsProcessState.isRunning) {
2485 TM_log('Process already running, ignoring');
2486 return;
2487 }
2488
2489 // Если это продолжение после остановки
2490 if (reviewsProcessState.shouldStop) {
2491 reviewsProcessState.shouldStop = false;
2492 generateBtn.textContent = 'Сгенерировать ответы на отзывы';
2493 }
2494
2495 reviewsProcessState.isRunning = true;
2496 reviewsProcessState.shouldStop = false;
2497 generateBtn.disabled = true;
2498 stopBtn.disabled = false;
2499
2500 TM_log('Starting reviews generation process...');
2501
2502 try {
2503 await processReviewsBatch();
2504 } catch (error) {
2505 console.error('Reviews generation error:', error);
2506 statusDiv.textContent = 'Ошибка при генерации ответов на отзывы';
2507 } finally {
2508 reviewsProcessState.isRunning = false;
2509 generateBtn.disabled = false;
2510 stopBtn.disabled = true;
2511 }
2512 }
2513
2514 // Функция для остановки генерации
2515 function stopReviewsGeneration() {
2516 TM_log('stopReviewsGeneration called');
2517 reviewsProcessState.shouldStop = true;
2518 const generateBtn = document.querySelector('.reviews-generate-btn');
2519 const stopBtn = document.querySelector('.reviews-stop-btn');
2520
2521 generateBtn.textContent = 'Продолжить';
2522 generateBtn.disabled = false;
2523 stopBtn.disabled = true;
2524
2525 const statusDiv = document.querySelector('.reviews-generation-status');
2526 statusDiv.textContent = `Остановлено. Обработано отзывов: ${reviewsProcessState.processedCount}`;
2527 }
2528
2529 // Функция для создания панели массовой генерации отзывов
2530 function createReviewsGenerationPanel() {
2531 console.log('createReviewsGenerationPanel called');
2532 const tableContainer = findReviewsTableContainer();
2533 console.log('Table container found:', !!tableContainer);
2534 if (!tableContainer) {
2535 console.error('Reviews table container not found');
2536 return;
2537 }
2538
2539 // Проверяем, не создана ли уже панель
2540 const existingPanel = document.querySelector('.reviews-generation-panel');
2541 console.log('Existing panel found:', !!existingPanel);
2542 if (existingPanel) {
2543 console.log('Reviews generation panel already exists');
2544 return;
2545 }
2546
2547 console.log('Creating reviews generation panel...');
2548 const panel = document.createElement('div');
2549 panel.className = 'reviews-generation-panel';
2550
2551 panel.innerHTML = `
2552 <div class="reviews-generation-title">💬 Массовая генерация ответов на отзывы</div>
2553 <div class="reviews-generation-buttons">
2554 <button class="reviews-generate-btn">Сгенерировать ответы на отзывы</button>
2555 <button class="reviews-stop-btn" disabled>Остановить</button>
2556 <button class="reviews-prompt-edit-btn">✏️ Промпт для отзывов</button>
2557 </div>
2558 <div class="reviews-generation-status"></div>
2559 `;
2560
2561 console.log('Inserting panel before table container...');
2562 tableContainer.insertAdjacentElement('beforebegin', panel);
2563 console.log('Panel inserted');
2564
2565 // Добавляем обработчики
2566 const generateBtn = panel.querySelector('.reviews-generate-btn');
2567 const stopBtn = panel.querySelector('.reviews-stop-btn');
2568 const promptEditBtn = panel.querySelector('.reviews-prompt-edit-btn');
2569
2570 TM_log('Adding event listeners to reviews panel buttons...');
2571
2572 generateBtn.addEventListener('click', () => {
2573 TM_log('Generate button clicked!');
2574 startReviewsGeneration();
2575 });
2576
2577 stopBtn.addEventListener('click', () => {
2578 TM_log('Stop button clicked!');
2579 stopReviewsGeneration();
2580 });
2581
2582 promptEditBtn.addEventListener('click', () => {
2583 TM_log('Prompt edit button clicked!');
2584 showReviewsPromptModal();
2585 });
2586
2587 TM_log('Reviews generation panel created with event listeners');
2588
2589 // Загружаем промпт для отзывов из localStorage
2590 loadReviewsPrompt();
2591 }
2592
2593 // Функция для генерации ответа на отзыв
2594 async function generateAnswerForReview(productName, rating, comment, sku) {
2595 // Ищем товар в базе знаний по SKU
2596 const productInfo = findProductInKnowledgeBase(sku);
2597 let knowledgeBaseInfo = '';
2598
2599 if (productInfo) {
2600 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
2601 console.log('Using knowledge base info for review generation');
2602 }
2603
2604 // Используем кастомный промпт или дефолтный
2605 const promptTemplate = getCurrentReviewsPrompt();
2606
2607 // Заменяем переменные в промпте
2608 const prompt = promptTemplate
2609 .replace('{productName}', productName)
2610 .replace('{rating}', rating.toString())
2611 .replace('{comment}', comment || 'Нет комментария')
2612 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo);
2613
2614 console.log('=== ПРОМПТ ДЛЯ ОТЗЫВА ===');
2615 console.log(prompt);
2616 console.log('=== КОНЕЦ ПРОМПТА ===');
2617
2618 const answer = await callAI(prompt);
2619
2620 console.log('=== ОТВЕТ ОТ AI ===');
2621 console.log(answer);
2622 console.log('=== КОНЕЦ ОТВЕТА ===');
2623
2624 return answer;
2625 }
2626
2627 // Функция ожидания открытия модального окна с отзывом
2628 function waitForReviewModal() {
2629 return new Promise((resolve) => {
2630 const checkModal = () => {
2631 // Ищем модальное окно с отзывом - обычно содержит textarea для ответа
2632 const modal = document.querySelector('[role="dialog"]');
2633 if (modal) {
2634 const textarea = modal.querySelector('textarea');
2635 if (textarea) {
2636 resolve();
2637 return;
2638 }
2639 }
2640 setTimeout(checkModal, 100);
2641 };
2642 checkModal();
2643 });
2644 }
2645
2646 // Функция для отправки ответа на отзыв
2647 async function submitReviewAnswer(answer) {
2648 // Находим модальное окно
2649 const modal = document.querySelector('[role="dialog"]');
2650 if (!modal) {
2651 throw new Error('Modal not found');
2652 }
2653
2654 // Находим textarea
2655 const textarea = modal.querySelector('textarea');
2656 if (!textarea) {
2657 throw new Error('Textarea not found');
2658 }
2659
2660 // Вставляем ответ
2661 textarea.value = answer;
2662
2663 // Генерируем события для обновления состояния
2664 const inputEvent = new Event('input', { bubbles: true });
2665 textarea.dispatchEvent(inputEvent);
2666
2667 const changeEvent = new Event('change', { bubbles: true });
2668 textarea.dispatchEvent(changeEvent);
2669
2670 textarea.focus();
2671 textarea.blur();
2672
2673 // Ждем немного для обновления UI
2674 await sleep(1000);
2675
2676 // Находим кнопку отправки
2677 const submitButton = Array.from(modal.querySelectorAll('button[type="submit"]'))
2678 .find(btn => btn.textContent.includes('Отправить') || btn.textContent.includes('Ответить'));
2679
2680 if (!submitButton) {
2681 throw new Error('Submit button not found');
2682 }
2683
2684 // Если кнопка disabled, пробуем еще раз обновить textarea
2685 if (submitButton.disabled) {
2686 textarea.value = answer;
2687 textarea.dispatchEvent(new Event('input', { bubbles: true }));
2688 textarea.dispatchEvent(new Event('change', { bubbles: true }));
2689 await sleep(500);
2690 }
2691
2692 submitButton.click();
2693
2694 // Ждем немного после отправки
2695 await sleep(1500);
2696 }
2697
2698 // Функция для закрытия модального окна с отзывом
2699 async function closeReviewModal() {
2700 // Ищем кнопку закрытия
2701 const closeButton = Array.from(document.querySelectorAll('button'))
2702 .find(btn => btn.getAttribute('aria-label') === 'Крестик для закрытия' ||
2703 btn.getAttribute('aria-label')?.includes('закры'));
2704
2705 if (closeButton) {
2706 closeButton.click();
2707 } else {
2708 // Альтернативный способ - нажатие ESC
2709 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27 }));
2710 }
2711
2712 await sleep(500);
2713 }
2714
2715 // Функция ожидания закрытия модального окна с отзывом
2716 function waitForReviewModalClose() {
2717 return new Promise((resolve) => {
2718 const checkModal = () => {
2719 const modal = document.querySelector('[role="dialog"]');
2720 if (!modal) {
2721 resolve();
2722 return;
2723 }
2724 setTimeout(checkModal, 100);
2725 };
2726 checkModal();
2727 });
2728 }
2729
2730 // Функция для обработки порции отзывов
2731 async function processReviewsBatch() {
2732 const statusDiv = document.querySelector('.reviews-generation-status');
2733
2734 while (!reviewsProcessState.shouldStop) {
2735 // Получаем видимые отзывы
2736 const reviews = getVisibleReviews();
2737
2738 if (reviews.length === 0) {
2739 statusDiv.textContent = `Завершено. Обработано отзывов: ${reviewsProcessState.processedCount}`;
2740 break;
2741 }
2742
2743 // Обрабатываем до 10 отзывов из текущей порции
2744 let processedInBatch = 0;
2745
2746 for (let i = 0; i < reviews.length && processedInBatch < 10; i++) {
2747 if (reviewsProcessState.shouldStop) {
2748 break;
2749 }
2750
2751 const review = reviews[i];
2752
2753 // Пропускаем отзывы, на которые уже есть ответы
2754 if (review.answersCount > 0) {
2755 console.log(`Review ${i + 1} skipped: already has ${review.answersCount} answer(s)`);
2756 continue;
2757 }
2758
2759 try {
2760 // Подсвечиваем текущий отзыв
2761 review.row.classList.add('review-row-processing');
2762
2763 statusDiv.textContent = `Обработка отзыва... Всего обработано: ${reviewsProcessState.processedCount}`;
2764
2765 // Кликаем на строку чтобы открыть модальное окно
2766 review.row.click();
2767
2768 // Ждем открытия модального окна
2769 await waitForReviewModal();
2770
2771 // Генерируем ответ
2772 const answer = await generateAnswerForReview(review.productName, review.rating, review.comment, review.sku);
2773
2774 // Проверяем валидность ответа
2775 if (!isValidAnswer(answer)) {
2776 console.log(`Review ${i + 1} skipped: AI returned invalid or SKIP answer`);
2777 review.row.classList.remove('review-row-processing');
2778 review.row.classList.add('review-row-error');
2779
2780 // Закрываем модальное окно
2781 await closeReviewModal();
2782 continue;
2783 }
2784
2785 // Вставляем ответ и отправляем
2786 await submitReviewAnswer(answer);
2787
2788 // Ждем закрытия модального окна
2789 await waitForReviewModalClose();
2790
2791 // Убираем подсветку обработки и добавляем подсветку завершения
2792 review.row.classList.remove('review-row-processing');
2793 review.row.classList.add('review-row-completed');
2794
2795 reviewsProcessState.processedCount++;
2796 processedInBatch++;
2797
2798 console.log(`Review ${i + 1} processed successfully`);
2799
2800 } catch (error) {
2801 console.error(`Error processing review ${i + 1}:`, error);
2802 review.row.classList.remove('review-row-processing');
2803 review.row.classList.add('review-row-error');
2804
2805 // Пытаемся закрыть модальное окно при ошибке
2806 try {
2807 await closeReviewModal();
2808 } catch (e) {
2809 console.error('Error closing modal:', e);
2810 }
2811 }
2812 }
2813
2814 // Если обработали меньше 10 отзывов, значит нужно проскроллить
2815 if (processedInBatch < 10 && !reviewsProcessState.shouldStop) {
2816 console.log('Scrolling to load more reviews...');
2817
2818 // Скроллим вниз
2819 const tableContainer = findReviewsTableContainer();
2820 if (tableContainer) {
2821 tableContainer.scrollTop = tableContainer.scrollHeight;
2822 }
2823
2824 // Ждем загрузки новых отзывов
2825 await sleep(2000);
2826
2827 // Проверяем, появились ли новые отзывы
2828 const newReviews = getVisibleReviews();
2829 if (newReviews.length === reviews.length) {
2830 // Новых отзывов не появилось, завершаем
2831 statusDiv.textContent = `Завершено. Обработано отзывов: ${reviewsProcessState.processedCount}`;
2832 break;
2833 }
2834 }
2835 }
2836
2837 if (reviewsProcessState.shouldStop) {
2838 statusDiv.textContent = `Остановлено. Обработано отзывов: ${reviewsProcessState.processedCount}`;
2839 }
2840 }
2841
2842 // Функция для старта генерации ответов на отзывы
2843 async function startReviewsGeneration() {
2844 TM_log('startReviewsGeneration called');
2845 const generateBtn = document.querySelector('.reviews-generate-btn');
2846 const stopBtn = document.querySelector('.reviews-stop-btn');
2847 const statusDiv = document.querySelector('.reviews-generation-status');
2848
2849 // Если процесс уже запущен - игнорируем
2850 if (reviewsProcessState.isRunning) {
2851 TM_log('Process already running, ignoring');
2852 return;
2853 }
2854
2855 // Если это продолжение после остановки
2856 if (reviewsProcessState.shouldStop) {
2857 reviewsProcessState.shouldStop = false;
2858 generateBtn.textContent = 'Сгенерировать ответы на отзывы';
2859 }
2860
2861 reviewsProcessState.isRunning = true;
2862 reviewsProcessState.shouldStop = false;
2863 generateBtn.disabled = true;
2864 stopBtn.disabled = false;
2865
2866 TM_log('Starting reviews generation process...');
2867
2868 try {
2869 await processReviewsBatch();
2870 } catch (error) {
2871 console.error('Reviews generation error:', error);
2872 statusDiv.textContent = 'Ошибка при генерации ответов на отзывы';
2873 } finally {
2874 reviewsProcessState.isRunning = false;
2875 generateBtn.disabled = false;
2876 stopBtn.disabled = true;
2877 }
2878 }
2879
2880 // Функция для остановки генерации
2881 function stopReviewsGeneration() {
2882 TM_log('stopReviewsGeneration called');
2883 reviewsProcessState.shouldStop = true;
2884 const generateBtn = document.querySelector('.reviews-generate-btn');
2885 const stopBtn = document.querySelector('.reviews-stop-btn');
2886
2887 generateBtn.textContent = 'Продолжить';
2888 generateBtn.disabled = false;
2889 stopBtn.disabled = true;
2890
2891 const statusDiv = document.querySelector('.reviews-generation-status');
2892 statusDiv.textContent = `Остановлено. Обработано отзывов: ${reviewsProcessState.processedCount}`;
2893 }
2894
2895 // Функция для создания панели массовой генерации отзывов
2896 function createReviewsGenerationPanel() {
2897 console.log('createReviewsGenerationPanel called');
2898 const tableContainer = findReviewsTableContainer();
2899 console.log('Table container found:', !!tableContainer);
2900 if (!tableContainer) {
2901 console.error('Reviews table container not found');
2902 return;
2903 }
2904
2905 // Проверяем, не создана ли уже панель
2906 const existingPanel = document.querySelector('.reviews-generation-panel');
2907 console.log('Existing panel found:', !!existingPanel);
2908 if (existingPanel) {
2909 console.log('Reviews generation panel already exists');
2910 return;
2911 }
2912
2913 console.log('Creating reviews generation panel...');
2914 const panel = document.createElement('div');
2915 panel.className = 'reviews-generation-panel';
2916
2917 panel.innerHTML = `
2918 <div class="reviews-generation-title">💬 Массовая генерация ответов на отзывы</div>
2919 <div class="reviews-generation-buttons">
2920 <button class="reviews-generate-btn">Сгенерировать ответы на отзывы</button>
2921 <button class="reviews-stop-btn" disabled>Остановить</button>
2922 <button class="reviews-prompt-edit-btn">✏️ Промпт для отзывов</button>
2923 </div>
2924 <div class="reviews-generation-status"></div>
2925 `;
2926
2927 console.log('Inserting panel before table container...');
2928 tableContainer.insertAdjacentElement('beforebegin', panel);
2929 console.log('Panel inserted');
2930
2931 // Добавляем обработчики
2932 const generateBtn = panel.querySelector('.reviews-generate-btn');
2933 const stopBtn = panel.querySelector('.reviews-stop-btn');
2934 const promptEditBtn = panel.querySelector('.reviews-prompt-edit-btn');
2935
2936 TM_log('Adding event listeners to reviews panel buttons...');
2937
2938 generateBtn.addEventListener('click', () => {
2939 TM_log('Generate button clicked!');
2940 startReviewsGeneration();
2941 });
2942
2943 stopBtn.addEventListener('click', () => {
2944 TM_log('Stop button clicked!');
2945 stopReviewsGeneration();
2946 });
2947
2948 promptEditBtn.addEventListener('click', () => {
2949 TM_log('Prompt edit button clicked!');
2950 showReviewsPromptModal();
2951 });
2952
2953 TM_log('Reviews generation panel created with event listeners');
2954
2955 // Загружаем промпт для отзывов из localStorage
2956 loadReviewsPrompt();
2957 }
2958
2959 // Инициализация массовой генерации отзывов
2960 function initReviewsGeneration() {
2961 // Проверяем, что мы на странице со списком отзывов
2962 if (!window.location.href.includes('/app/reviews') ||
2963 window.location.href.includes('/app/reviews/questions') ||
2964 window.location.href.includes('/app/reviews/auto-answer')) {
2965 return;
2966 }
2967
2968 console.log('Initializing reviews generation...');
2969
2970 addReviewsGenerationStyles();
2971
2972 // Ждем загрузки таблицы
2973 const observer = new MutationObserver(debounce(() => {
2974 const tableContainer = findReviewsTableContainer();
2975 const existingPanel = document.querySelector('.reviews-generation-panel');
2976
2977 if (tableContainer) {
2978 // Если панель существует, но находится не перед контейнером таблицы
2979 if (existingPanel) {
2980 // Проверяем, находится ли панель перед текущим контейнером таблицы
2981 const panelNextSibling = existingPanel.nextElementSibling;
2982 if (panelNextSibling !== tableContainer) {
2983 console.log('Panel found but in wrong position, moving it...');
2984 tableContainer.insertAdjacentElement('beforebegin', existingPanel);
2985 console.log('Panel moved to correct position');
2986 }
2987 } else {
2988 console.log('Panel not found, creating new one...');
2989 createReviewsGenerationPanel();
2990 }
2991 }
2992 }, 300));
2993
2994 observer.observe(document.body, {
2995 childList: true,
2996 subtree: true
2997 });
2998
2999 // Пробуем создать панель сразу
3000 const tableContainer = findReviewsTableContainer();
3001 if (tableContainer) {
3002 createReviewsGenerationPanel();
3003 }
3004 }
3005
3006 // Запускаем инициализацию массовой генерации отзывов
3007 initReviewsGeneration();
3008
3009})();