Size
76.9 KB
Version
1.3.20
Created
Mar 17, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Ozon AI Answer Generator
3// @description AI-powered answer generator for Ozon seller questions
4// @version 1.3.20
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 console.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 console.log('AI Generator UI already exists');
154 return;
155 }
156
157 // Находим заголовок "Ответ на вопрос"
158 const answerTitleContainer = modal.querySelector('.mt7');
159 if (!answerTitleContainer) {
160 console.error('Answer title container not found in modal');
161 return;
162 }
163
164 // Находим textarea для ответа (для проверки)
165 const textarea = modal.querySelector('textarea');
166 if (!textarea) {
167 console.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 console.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('.c7r110-a.c7r110-b3.body-500'))
209 .find(el => el.textContent.trim() === 'Артикул');
210 const articleContainer = articleLabel ? articleLabel.closest('.n1d-a1e') : null;
211 const articleElement = articleContainer ? articleContainer.querySelector('.c7r110-a.c7r110-b2.body-500') : null;
212 const sellerArticle = articleElement ? articleElement.textContent.trim() : '';
213
214 console.log('Seller article:', sellerArticle);
215
216 // Получаем текст вопроса - исправленный селектор
217 const questionLabel = Array.from(modal.querySelectorAll('.c7r110-a.c7r110-b3.body-500'))
218 .find(el => el.textContent.trim() === 'Вопрос');
219 const questionContainer = questionLabel ? questionLabel.closest('.n1d-a1e') : null;
220 const questionTextContainer = questionContainer ? questionContainer.querySelector('.n1d-ae3') : null;
221 const questionElement = questionTextContainer ? questionTextContainer.querySelector('.c7r110-a.c7r110-b2.body-500') : null;
222 const questionText = questionElement ? questionElement.textContent.trim() : '';
223
224 if (!questionText) {
225 console.error('Question not found. Container:', questionContainer);
226 console.error('Text container:', questionTextContainer);
227 console.error('Question element:', questionElement);
228 throw new Error('Не удалось найти текст вопроса');
229 }
230
231 console.log('Product:', productName);
232 console.log('Question:', questionText);
233
234 // Получаем дополнительный промпт
235 const additionalPrompt = promptInput.value.trim();
236 console.log('Additional prompt:', additionalPrompt);
237
238 // Ищем товар в базе знаний по артикулу продавца
239 const productInfo = findProductInKnowledgeBase(sellerArticle);
240 let knowledgeBaseInfo = '';
241
242 if (productInfo) {
243 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
244 console.log('Using knowledge base info for product');
245 }
246
247 // Используем кастомный промпт или дефолтный
248 const promptTemplate = getCurrentPrompt();
249
250 // Заменяем переменные в промпте
251 let mainPrompt = promptTemplate
252 .replace('{productName}', productName)
253 .replace('{questionText}', questionText)
254 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo)
255 .replace('{additionalPrompt}', additionalPrompt);
256
257 console.log('Calling AI with prompt...');
258 console.log('Product name:', productName);
259 console.log('Question:', questionText);
260 console.log('Knowledge base info:', knowledgeBaseInfo);
261 console.log('Full prompt:', mainPrompt);
262
263 // Вызываем AI
264 const answer = await callAI(mainPrompt);
265
266 console.log('AI Response:', answer);
267
268 // Проверяем валидность ответа
269 if (!isValidAnswer(answer)) {
270 console.log('AI returned invalid or SKIP answer');
271 alert('AI не смог сгенерировать ответ: недостаточно информации о товаре');
272 return;
273 }
274
275 // Вставляем ответ в textarea
276 textarea.value = answer;
277
278 // Генерируем событие input для обновления состояния формы
279 const inputEvent = new Event('input', { bubbles: true });
280 textarea.dispatchEvent(inputEvent);
281
282 // Меняем текст кнопки на "Перегенерировать"
283 button.textContent = 'Перегенерировать ответ';
284 button.setAttribute('data-generated', 'true');
285
286 console.log('Answer generated and inserted successfully');
287
288 } catch (error) {
289 console.error('Error generating answer:', error);
290 alert('Ошибка при генерации ответа: ' + error.message);
291 } finally {
292 // Включаем кнопку обратно
293 button.disabled = false;
294 button.classList.remove('loading');
295 }
296 }
297
298 // Функция для проверки, является ли ответ валидным
299 function isValidAnswer(answer) {
300 if (!answer || typeof answer !== 'string') {
301 return false;
302 }
303
304 const trimmedAnswer = answer.trim().toUpperCase();
305
306 // Проверяем на SKIP
307 if (trimmedAnswer === 'SKIP') {
308 return false;
309 }
310
311 // Проверяем на слишком короткий ответ (меньше 10 символов)
312 if (answer.trim().length < 10) {
313 return false;
314 }
315
316 // Проверяем на типичные фразы отказа
317 const refusalPhrases = [
318 'не могу ответить',
319 'недостаточно информации',
320 'не знаю',
321 'cannot answer',
322 'don\'t know',
323 'insufficient information',
324 'нет информации'
325 ];
326
327 const lowerAnswer = answer.toLowerCase();
328 for (const phrase of refusalPhrases) {
329 if (lowerAnswer.includes(phrase)) {
330 return false;
331 }
332 }
333
334 return true;
335 }
336
337 // Наблюдатель за появлением модального окна
338 function observeModal() {
339 const observer = new MutationObserver(debounce(() => {
340 const modal = document.querySelector('.ct3110-a');
341 if (modal) {
342 // Проверяем, что это модальное окно с вопросом
343 const modalTitle = modal.querySelector('.ct3110-a6.heading-500');
344 if (modalTitle && modalTitle.textContent.includes('Вопрос о товаре')) {
345 console.log('Question modal detected');
346 createGeneratorUI(modal);
347 }
348 }
349 }, 300));
350
351 observer.observe(document.body, {
352 childList: true,
353 subtree: true
354 });
355
356 console.log('Modal observer started');
357 }
358
359 // Инициализация
360 function init() {
361 console.log('Starting Ozon AI Answer Generator...');
362
363 // Добавляем стили
364 addStyles();
365
366 // Проверяем, открыто ли уже модальное окно
367 const existingModal = document.querySelector('.ct3110-a');
368 if (existingModal) {
369 const modalTitle = existingModal.querySelector('.ct3110-a6.heading-500');
370 if (modalTitle && modalTitle.textContent.includes('Вопрос о товаре')) {
371 console.log('Existing question modal found');
372 createGeneratorUI(existingModal);
373 }
374 }
375
376 // Запускаем наблюдатель
377 observeModal();
378 }
379
380 // Запускаем после загрузки DOM
381 if (document.readyState === 'loading') {
382 document.addEventListener('DOMContentLoaded', init);
383 } else {
384 init();
385 }
386
387 // ============= МАССОВАЯ ГЕНЕРАЦИЯ ОТВЕТОВ =============
388
389 // Хранилище для сгенерированных ответов
390 let generatedAnswers = new Map();
391
392 // Хранилище базы знаний
393 let knowledgeBase = null;
394
395 // Хранилище промпта
396 let customPrompt = null;
397
398 // Хранилище настроек модели
399 let modelSettings = {
400 provider: 'rmcall', // 'rmcall' или 'openrouter'
401 model: 'google/gemini-2.0-flash-exp:free',
402 apiKey: '',
403 customModels: [] // Список кастомных моделей
404 };
405
406 // Дефолтный промпт
407 const DEFAULT_PROMPT = 'Ты - профессиональный менеджер по работе с клиентами на маркетплейсе Ozon.\n\n' +
408 'Продукт: {productName}\n\n' +
409 'Вопрос покупателя: {questionText}{knowledgeBaseInfo}\n\n' +
410 'Задача: Сгенерируй профессиональный, вежливый и информативный ответ на вопрос покупателя о товаре.\n\n' +
411 'Требования к ответу:\n' +
412 '- Будь вежливым и дружелюбным\n' +
413 '- Отвечай по существу вопроса\n' +
414 '- Используй информацию о продукте\n' +
415 '- Ответ должен быть кратким (2-4 предложения)\n' +
416 '- Не придумывай характеристики, которых нет в названии продукта\n' +
417 '- Если нужна дополнительная информация, вежливо предложи обратиться к описанию товара\n' +
418 '- Учитывай при ответе Дополнительные инструкции {additionalPrompt} если они есть\n' +
419 '- ВАЖНО: Если у тебя недостаточно информации для ответа или ты не знаешь что ответить, напиши ТОЛЬКО слово "SKIP" без дополнительных объяснений';
420
421 // Функция для загрузки базы знаний из localStorage
422 async function loadKnowledgeBase() {
423 try {
424 const stored = localStorage.getItem('ozon_knowledge_base');
425 if (stored) {
426 knowledgeBase = JSON.parse(stored);
427 console.log('Knowledge base loaded:', knowledgeBase.length, 'products');
428 updateKnowledgeBaseStatus();
429 }
430 } catch (error) {
431 console.error('Error loading knowledge base:', error);
432 }
433 }
434
435 // Функция для сохранения базы знаний в localStorage
436 function saveKnowledgeBase(data) {
437 try {
438 localStorage.setItem('ozon_knowledge_base', JSON.stringify(data));
439 knowledgeBase = data;
440 console.log('Knowledge base saved:', data.length, 'products');
441 updateKnowledgeBaseStatus();
442 } catch (error) {
443 console.error('Error saving knowledge base:', error);
444 alert('Ошибка при сохранении базы знаний: ' + error.message);
445 }
446 }
447
448 // Функция для обновления статуса базы знаний
449 function updateKnowledgeBaseStatus() {
450 const statusDiv = document.querySelector('.kb-status');
451 if (statusDiv) {
452 if (knowledgeBase && knowledgeBase.length > 0) {
453 statusDiv.textContent = `База знаний загружена: ${knowledgeBase.length} товаров`;
454 statusDiv.style.color = '#28a745';
455 } else {
456 statusDiv.textContent = 'База знаний не загружена';
457 statusDiv.style.color = '#666';
458 }
459 }
460 }
461
462 // Функция для поиска товара в базе знаний по SKU
463 function findProductInKnowledgeBase(sellerArticle) {
464 if (!knowledgeBase || knowledgeBase.length === 0) {
465 console.log('Knowledge base is empty');
466 return null;
467 }
468
469 if (!sellerArticle) {
470 console.log('Seller article is empty');
471 return null;
472 }
473
474 console.log('Searching for seller article:', sellerArticle);
475
476 // Ищем товар по артикулу (штрихкод)
477 const product = knowledgeBase.find(item => {
478 const itemSku = item['Штрихкод (Серийный номер / EAN)'];
479 return itemSku && itemSku.toString().trim() === sellerArticle;
480 });
481
482 if (product) {
483 console.log('Product found in knowledge base:', product);
484 } else {
485 console.log('Product not found in knowledge base for article:', sellerArticle);
486 }
487
488 return product;
489 }
490
491 // Функция для форматирования информации о товаре из базы знаний
492 function formatProductInfo(product) {
493 const info = [];
494
495 if (product['Название товара']) info.push(`Название: ${product['Название товара']}`);
496 if (product['Бренд*']) info.push(`Бренд: ${product['Бренд*']}`);
497 if (product['Тип*']) info.push(`Тип: ${product['Тип*']}`);
498 if (product['Состав*']) info.push(`Состав: ${product['Состав*']}`);
499 if (product['Аннотация']) info.push(`Описание: ${product['Аннотация']}`);
500 if (product['Целевая аудитория']) info.push(`Целевая аудитория: ${product['Целевая аудитория']}`);
501 if (product['Направление БАД']) info.push(`Направление: ${product['Направление БАД']}`);
502 if (product['Вкусовой акцент (вкус)']) info.push(`Вкус: ${product['Вкусовой акцент (вкус)']}`);
503 if (product['Форма выпуска продукта']) info.push(`Форма выпуска: ${product['Форма выпуска продукта']}`);
504 if (product['Способ применения']) info.push(`Способ применения: ${product['Способ применения']}`);
505 if (product['Срок годности в днях']) info.push(`Срок годности: ${product['Срок годности в днях']} дней`);
506 if (product['Страна-изготовитель']) info.push(`Страна-изготовитель: ${product['Страна-изготовитель']}`);
507 if (product['Для детей']) info.push(`Для детей: ${product['Для детей']}`);
508 if (product['Минимальный возраст от']) info.push(`Минимальный возраст: ${product['Минимальный возраст от']}`);
509
510 return info.join('\n');
511 }
512
513 // Функция для обработки загрузки XLS файла
514 function handleFileUpload(event) {
515 const file = event.target.files[0];
516 if (!file) return;
517
518 const reader = new FileReader();
519
520 reader.onload = function(e) {
521 try {
522 const data = new Uint8Array(e.target.result);
523 const workbook = XLSX.read(data, { type: 'array' });
524
525 // Берем первый лист
526 const firstSheetName = workbook.SheetNames[0];
527 const worksheet = workbook.Sheets[firstSheetName];
528
529 // Конвертируем в JSON
530 const jsonData = XLSX.utils.sheet_to_json(worksheet);
531
532 console.log('XLS parsed:', jsonData.length, 'rows');
533
534 if (jsonData.length === 0) {
535 alert('Файл пустой или не содержит данных');
536 return;
537 }
538
539 // Сохраняем в localStorage
540 saveKnowledgeBase(jsonData);
541 alert(`База знаний загружена успешно!\nЗагружено товаров: ${jsonData.length}`);
542
543 } catch (error) {
544 console.error('Error parsing XLS:', error);
545 alert('Ошибка при чтении файла: ' + error.message);
546 }
547 };
548
549 reader.onerror = function(error) {
550 console.error('Error reading file:', error);
551 alert('Ошибка при чтении файла');
552 };
553
554 reader.readAsArrayBuffer(file);
555 }
556
557 // Функция для добавления стилей массовой генерации
558 function addBulkGenerationStyles() {
559 const styles = `
560 .bulk-generation-panel {
561 margin: 20px 0;
562 padding: 20px;
563 background: #ffffff;
564 border-radius: 8px;
565 border: 2px solid #005bff;
566 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
567 }
568
569 .bulk-generation-title {
570 font-size: 16px;
571 font-weight: 600;
572 margin-bottom: 16px;
573 color: #1a1a1a;
574 }
575
576 .bulk-generation-buttons {
577 display: flex;
578 gap: 12px;
579 margin-bottom: 12px;
580 }
581
582 .bulk-generate-btn, .bulk-answer-all-btn {
583 background: #005bff;
584 color: white;
585 border: none;
586 padding: 12px 24px;
587 border-radius: 6px;
588 font-size: 14px;
589 font-weight: 500;
590 cursor: pointer;
591 transition: background 0.2s;
592 }
593
594 .bulk-generate-btn:hover, .bulk-answer-all-btn:hover {
595 background: #0047cc;
596 }
597
598 .bulk-generate-btn:disabled, .bulk-answer-all-btn:disabled {
599 background: #d1d1d6;
600 cursor: not-allowed;
601 }
602
603 .bulk-answer-all-btn {
604 background: #28a745;
605 }
606
607 .bulk-answer-all-btn:hover {
608 background: #218838;
609 }
610
611 .kb-upload-btn {
612 background: #6f42c1;
613 color: white;
614 border: none;
615 padding: 12px 24px;
616 border-radius: 6px;
617 font-size: 14px;
618 font-weight: 500;
619 cursor: pointer;
620 transition: background 0.2s;
621 }
622
623 .kb-upload-btn:hover {
624 background: #5a32a3;
625 }
626
627 .prompt-edit-btn {
628 background: #fd7e14;
629 color: white;
630 border: none;
631 padding: 12px 24px;
632 border-radius: 6px;
633 font-size: 14px;
634 font-weight: 500;
635 cursor: pointer;
636 transition: background 0.2s;
637 }
638
639 .prompt-edit-btn:hover {
640 background: #e8590c;
641 }
642
643 .model-select-btn {
644 background: #17a2b8;
645 color: white;
646 border: none;
647 padding: 12px 24px;
648 border-radius: 6px;
649 font-size: 14px;
650 font-weight: 500;
651 cursor: pointer;
652 transition: background 0.2s;
653 }
654
655 .model-select-btn:hover {
656 background: #138496;
657 }
658
659 .kb-file-input {
660 display: none;
661 }
662
663 .prompt-modal-overlay {
664 position: fixed;
665 top: 0;
666 left: 0;
667 right: 0;
668 bottom: 0;
669 background: rgba(0, 0, 0, 0.5);
670 display: flex;
671 align-items: center;
672 justify-content: center;
673 z-index: 10000;
674 }
675
676 .prompt-modal {
677 background: white;
678 border-radius: 8px;
679 width: 90%;
680 max-width: 800px;
681 max-height: 90vh;
682 display: flex;
683 flex-direction: column;
684 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
685 }
686
687 .prompt-modal-header {
688 display: flex;
689 justify-content: space-between;
690 align-items: center;
691 padding: 20px;
692 border-bottom: 1px solid #e0e0e0;
693 }
694
695 .prompt-modal-title {
696 margin: 0;
697 font-size: 18px;
698 font-weight: 600;
699 color: #1a1a1a;
700 }
701
702 .prompt-modal-close {
703 background: none;
704 border: none;
705 font-size: 24px;
706 color: #666;
707 cursor: pointer;
708 padding: 0;
709 width: 30px;
710 height: 30px;
711 display: flex;
712 align-items: center;
713 justify-content: center;
714 border-radius: 4px;
715 transition: background 0.2s;
716 }
717
718 .prompt-modal-close:hover {
719 background: #f0f0f0;
720 }
721
722 .prompt-modal-body {
723 padding: 20px;
724 flex: 1;
725 overflow-y: auto;
726 }
727
728 .prompt-modal-hint {
729 font-size: 13px;
730 color: #666;
731 margin-bottom: 12px;
732 padding: 10px;
733 background: #f8f9fa;
734 border-radius: 4px;
735 border-left: 3px solid #fd7e14;
736 }
737
738 .prompt-modal-textarea {
739 width: 100%;
740 min-height: 300px;
741 padding: 12px;
742 border: 1px solid #d1d1d6;
743 border-radius: 6px;
744 font-size: 14px;
745 font-family: 'Courier New', monospace;
746 resize: vertical;
747 box-sizing: border-box;
748 }
749
750 .prompt-modal-textarea:focus {
751 outline: none;
752 border-color: #fd7e14;
753 box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.1);
754 }
755
756 .prompt-modal-footer {
757 display: flex;
758 justify-content: space-between;
759 align-items: center;
760 padding: 20px;
761 border-top: 1px solid #e0e0e0;
762 }
763
764 .prompt-modal-actions {
765 display: flex;
766 gap: 12px;
767 }
768
769 .prompt-modal-reset {
770 background: #6c757d;
771 color: white;
772 border: none;
773 padding: 10px 20px;
774 border-radius: 6px;
775 font-size: 14px;
776 font-weight: 500;
777 cursor: pointer;
778 transition: background 0.2s;
779 }
780
781 .prompt-modal-reset:hover {
782 background: #5a6268;
783 }
784
785 .prompt-modal-cancel {
786 background: #f0f0f0;
787 color: #333;
788 border: none;
789 padding: 10px 20px;
790 border-radius: 6px;
791 font-size: 14px;
792 font-weight: 500;
793 cursor: pointer;
794 transition: background 0.2s;
795 }
796
797 .prompt-modal-cancel:hover {
798 background: #e0e0e0;
799 }
800
801 .prompt-modal-save {
802 background: #fd7e14;
803 color: white;
804 border: none;
805 padding: 10px 20px;
806 border-radius: 6px;
807 font-size: 14px;
808 font-weight: 500;
809 cursor: pointer;
810 transition: background 0.2s;
811 }
812
813 .prompt-modal-save:hover {
814 background: #e8590c;
815 }
816
817 .model-modal-form-group {
818 margin-bottom: 20px;
819 }
820
821 .model-modal-label {
822 display: block;
823 font-size: 14px;
824 font-weight: 600;
825 color: #1a1a1a;
826 margin-bottom: 8px;
827 }
828
829 .model-modal-select {
830 width: 100%;
831 padding: 10px 12px;
832 border: 1px solid #d1d1d6;
833 border-radius: 6px;
834 font-size: 14px;
835 font-family: inherit;
836 box-sizing: border-box;
837 background: white;
838 }
839
840 .model-modal-select:focus {
841 outline: none;
842 border-color: #17a2b8;
843 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
844 }
845
846 .model-modal-input {
847 width: 100%;
848 padding: 10px 12px;
849 border: 1px solid #d1d1d6;
850 border-radius: 6px;
851 font-size: 14px;
852 font-family: inherit;
853 box-sizing: border-box;
854 }
855
856 .model-modal-input:focus {
857 outline: none;
858 border-color: #17a2b8;
859 box-shadow: 0 0 0 3px rgba(23, 162, 184, 0.1);
860 }
861
862 .model-modal-input:disabled {
863 background: #f5f5f5;
864 cursor: not-allowed;
865 }
866 `;
867
868 TM_addStyle(styles);
869 }
870
871 // Функция для создания панели массовой генерации
872 function createBulkGenerationPanel() {
873 const tableContainer = document.querySelector('.n1d-aup3');
874 if (!tableContainer) {
875 console.error('Table container not found');
876 return;
877 }
878
879 // Проверяем, не создана ли уже панель
880 if (document.querySelector('.bulk-generation-panel')) {
881 console.log('Bulk generation panel already exists');
882 return;
883 }
884
885 const panel = document.createElement('div');
886 panel.className = 'bulk-generation-panel';
887
888 panel.innerHTML = `
889 <div class="bulk-generation-title">🤖 Массовая генерация ответов</div>
890 <div class="bulk-generation-buttons">
891 <button class="bulk-generate-btn">Сгенерировать ответы</button>
892 <button class="bulk-answer-all-btn" disabled>Ответить всем</button>
893 <button class="kb-upload-btn">📚 Загрузить базу знаний</button>
894 <button class="prompt-edit-btn">✏️ Промпт</button>
895 <button class="model-select-btn">⚙️ Модель</button>
896 <input type="file" class="kb-file-input" accept=".xls,.xlsx" />
897 </div>
898 <div class="bulk-generation-status"></div>
899 <div class="kb-status">База знаний не загружена</div>
900 `;
901
902 tableContainer.insertAdjacentElement('beforebegin', panel);
903
904 // Добавляем обработчики
905 const generateBtn = panel.querySelector('.bulk-generate-btn');
906 const answerAllBtn = panel.querySelector('.bulk-answer-all-btn');
907 const kbUploadBtn = panel.querySelector('.kb-upload-btn');
908 const promptEditBtn = panel.querySelector('.prompt-edit-btn');
909 const kbFileInput = panel.querySelector('.kb-file-input');
910
911 generateBtn.addEventListener('click', () => bulkGenerateAnswers());
912 answerAllBtn.addEventListener('click', () => bulkAnswerAll());
913
914 kbUploadBtn.addEventListener('click', () => {
915 kbFileInput.click();
916 });
917
918 promptEditBtn.addEventListener('click', () => {
919 showPromptModal();
920 });
921
922 const modelSelectBtn = panel.querySelector('.model-select-btn');
923
924 modelSelectBtn.addEventListener('click', () => {
925 showModelModal();
926 });
927
928 kbFileInput.addEventListener('change', handleFileUpload);
929
930 console.log('Bulk generation panel created');
931
932 // Загружаем базу знаний из localStorage
933 loadKnowledgeBase();
934
935 // Загружаем кастомный промпт из localStorage
936 loadCustomPrompt();
937 }
938
939 // Функция для получения всех видимых вопросов
940 function getVisibleQuestions() {
941 const questionRows = document.querySelectorAll('tr.ct5110-c.ct5110-b8');
942 const questions = [];
943
944 questionRows.forEach((row, index) => {
945 const productLink = row.querySelector('td:nth-child(3) a');
946 const questionButton = row.querySelector('td:nth-child(4) button');
947 const skuElement = row.querySelector('td:nth-child(3) .c7r110-a.c7r110-b5.body-400');
948
949 // Получаем количество ответов из 5-й колонки
950 const answersCountElement = row.querySelector('td:nth-child(5)');
951 const answersCount = answersCountElement ? parseInt(answersCountElement.textContent.trim()) : 0;
952
953 if (productLink && questionButton) {
954 questions.push({
955 index: index,
956 row: row,
957 productName: productLink.textContent.trim(),
958 questionText: questionButton.textContent.trim(),
959 questionButton: questionButton,
960 sku: skuElement ? skuElement.textContent.trim() : '',
961 answersCount: answersCount
962 });
963 }
964 });
965
966 return questions;
967 }
968
969 // Функция для массовой генерации ответов
970 async function bulkGenerateAnswers() {
971 const generateBtn = document.querySelector('.bulk-generate-btn');
972 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
973 const statusDiv = document.querySelector('.bulk-generation-status');
974
975 try {
976 generateBtn.disabled = true;
977 answerAllBtn.disabled = true;
978 generatedAnswers.clear();
979
980 const questions = getVisibleQuestions();
981
982 if (questions.length === 0) {
983 statusDiv.textContent = 'Нет вопросов для обработки';
984 return;
985 }
986
987 statusDiv.textContent = `Генерация ответов: 0 из ${questions.length}`;
988
989 let skippedCount = 0;
990 let alreadyAnsweredCount = 0;
991
992 for (let i = 0; i < questions.length; i++) {
993 const question = questions[i];
994
995 try {
996 // Пропускаем вопросы, на которые уже есть ответы
997 if (question.answersCount > 0) {
998 console.log(`Question ${i + 1} skipped: already has ${question.answersCount} answer(s)`);
999 alreadyAnsweredCount++;
1000 continue;
1001 }
1002
1003 // Подсвечиваем текущий вопрос
1004 question.row.classList.add('question-row-processing');
1005
1006 statusDiv.textContent = `Генерация ответов: ${i + 1} из ${questions.length}`;
1007
1008 // Генерируем ответ
1009 const answer = await generateAnswerForQuestion(question.productName, question.questionText, question.sku);
1010
1011 // Проверяем валидность ответа
1012 if (!isValidAnswer(answer)) {
1013 console.log(`Question ${i + 1} skipped: AI returned invalid or SKIP answer`);
1014 question.row.classList.remove('question-row-processing');
1015 question.row.classList.add('question-row-error');
1016 skippedCount++;
1017 continue;
1018 }
1019
1020 // Сохраняем ответ
1021 generatedAnswers.set(i, answer);
1022
1023 // Добавляем textarea с ответом
1024 addAnswerTextarea(question.row, answer, i);
1025
1026 // Убираем подсветку обработки и добавляем подсветку завершения
1027 question.row.classList.remove('question-row-processing');
1028 question.row.classList.add('question-row-completed');
1029
1030 console.log(`Answer generated for question ${i + 1}:`, answer);
1031
1032 } catch (error) {
1033 console.error(`Error generating answer for question ${i + 1}:`, error);
1034 question.row.classList.remove('question-row-processing');
1035 question.row.classList.add('question-row-error');
1036 skippedCount++;
1037 // Пропускаем вопрос при ошибке
1038 }
1039 }
1040
1041 let statusText = `Генерация завершена: ${generatedAnswers.size} из ${questions.length} ответов`;
1042 if (alreadyAnsweredCount > 0) {
1043 statusText += ` (уже отвечено: ${alreadyAnsweredCount})`;
1044 }
1045 if (skippedCount > 0) {
1046 statusText += ` (пропущено: ${skippedCount})`;
1047 }
1048 statusDiv.textContent = statusText;
1049 answerAllBtn.disabled = generatedAnswers.size === 0;
1050
1051 } catch (error) {
1052 console.error('Bulk generation error:', error);
1053 statusDiv.textContent = 'Ошибка при массовой генерации';
1054 } finally {
1055 generateBtn.disabled = false;
1056 }
1057 }
1058
1059 // Функция для генерации ответа на один вопрос
1060 async function generateAnswerForQuestion(productName, questionText, sku) {
1061 // Ищем товар в базе знаний по SKU
1062 const productInfo = findProductInKnowledgeBase(sku);
1063 let knowledgeBaseInfo = '';
1064
1065 if (productInfo) {
1066 knowledgeBaseInfo = '\n\nДополнительная информация о товаре из базы знаний:\n' + formatProductInfo(productInfo);
1067 console.log('Using knowledge base info for bulk generation');
1068 }
1069
1070 // Используем кастомный промпт или дефолтный
1071 const promptTemplate = getCurrentPrompt();
1072
1073 // Заменяем переменные в промпте
1074 const prompt = promptTemplate
1075 .replace('{productName}', productName)
1076 .replace('{questionText}', questionText)
1077 .replace('{knowledgeBaseInfo}', knowledgeBaseInfo)
1078 .replace('{additionalPrompt}', '');
1079
1080 const answer = await callAI(prompt);
1081 return answer;
1082 }
1083
1084 // Функция для добавления textarea с ответом
1085 function addAnswerTextarea(row, answer, index) {
1086 // Проверяем, не добавлен ли уже textarea
1087 const existingContainer = row.querySelector('.answer-textarea-container');
1088 if (existingContainer) {
1089 const textarea = existingContainer.querySelector('.answer-textarea');
1090 textarea.value = answer;
1091 autoResizeTextarea(textarea);
1092 return;
1093 }
1094
1095 const questionCell = row.querySelector('td:nth-child(4)');
1096 if (!questionCell) return;
1097
1098 const container = document.createElement('div');
1099 container.className = 'answer-textarea-container';
1100
1101 container.innerHTML = `
1102 <div class="answer-label">Сгенерированный ответ:</div>
1103 <textarea class="answer-textarea" data-question-index="${index}">${answer}</textarea>
1104 `;
1105
1106 questionCell.appendChild(container);
1107
1108 // Обновляем значение в Map при редактировании
1109 const textarea = container.querySelector('.answer-textarea');
1110
1111 // Автоматически изменяем размер textarea
1112 autoResizeTextarea(textarea);
1113
1114 // Предотвращаем открытие модального окна при клике на textarea или контейнер
1115 container.addEventListener('click', (e) => {
1116 e.stopPropagation();
1117 });
1118
1119 textarea.addEventListener('click', (e) => {
1120 e.stopPropagation();
1121 });
1122
1123 textarea.addEventListener('input', () => {
1124 generatedAnswers.set(index, textarea.value);
1125 autoResizeTextarea(textarea);
1126 updateAnswerAllButton();
1127 });
1128
1129 // Активируем кнопку "Ответить всем"
1130 updateAnswerAllButton();
1131 }
1132
1133 // Функция для обновления состояния кнопки "Ответить всем"
1134 function updateAnswerAllButton() {
1135 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1136 if (!answerAllBtn) return;
1137
1138 // Проверяем есть ли хотя бы один ответ
1139 const questions = getVisibleQuestions();
1140 let hasAnswers = false;
1141
1142 for (const question of questions) {
1143 const answerTextarea = question.row.querySelector('.answer-textarea');
1144 if (answerTextarea && answerTextarea.value.trim()) {
1145 hasAnswers = true;
1146 break;
1147 }
1148 }
1149
1150 answerAllBtn.disabled = !hasAnswers;
1151 }
1152
1153 // Функция для автоматического изменения размера textarea
1154 function autoResizeTextarea(textarea) {
1155 // Сбрасываем высоту для правильного расчета
1156 textarea.style.height = 'auto';
1157
1158 // Устанавливаем высоту на основе scrollHeight
1159 const newHeight = Math.max(150, textarea.scrollHeight);
1160 textarea.style.height = newHeight + 'px';
1161
1162 // Также адаптируем ширину, если текст очень длинный
1163 const lineLength = textarea.value.split('\n').reduce((max, line) => Math.max(max, line.length), 0);
1164 if (lineLength > 100) {
1165 textarea.style.width = '100%';
1166 }
1167 }
1168
1169 // Функция для автоматической отправки всех ответов
1170 async function bulkAnswerAll() {
1171 const answerAllBtn = document.querySelector('.bulk-answer-all-btn');
1172 const statusDiv = document.querySelector('.bulk-generation-status');
1173
1174 try {
1175 answerAllBtn.disabled = true;
1176
1177 const questions = getVisibleQuestions();
1178 let successCount = 0;
1179 let totalToSend = 0;
1180
1181 // Сначала подсчитываем сколько ответов нужно отправить
1182 for (let i = 0; i < questions.length; i++) {
1183 const question = questions[i];
1184 const answerTextarea = question.row.querySelector('.answer-textarea');
1185 if (answerTextarea && answerTextarea.value.trim()) {
1186 totalToSend++;
1187 }
1188 }
1189
1190 if (totalToSend === 0) {
1191 statusDiv.textContent = 'Нет ответов для отправки';
1192 answerAllBtn.disabled = false;
1193 return;
1194 }
1195
1196 statusDiv.textContent = `Отправка ответов: 0 из ${totalToSend}`;
1197
1198 for (let i = 0; i < questions.length; i++) {
1199 const question = questions[i];
1200
1201 // Проверяем есть ли textarea с ответом
1202 const answerTextarea = question.row.querySelector('.answer-textarea');
1203 if (!answerTextarea || !answerTextarea.value.trim()) {
1204 console.log(`Question ${i + 1} skipped: no answer`);
1205 continue;
1206 }
1207
1208 const answer = answerTextarea.value.trim();
1209
1210 try {
1211 console.log(`Processing question ${i + 1}, answer:`, answer);
1212 statusDiv.textContent = `Отправка ответов: ${successCount + 1} из ${totalToSend}`;
1213
1214 // Кликаем на вопрос
1215 question.questionButton.click();
1216 console.log('Clicked on question button');
1217
1218 // Ждем открытия модального окна
1219 await waitForModal();
1220 console.log('Modal opened');
1221
1222 // Находим textarea и вставляем ответ
1223 const modal = document.querySelector('.ct3110-a');
1224 if (!modal) {
1225 throw new Error('Modal not found');
1226 }
1227
1228 const textarea = modal.querySelector('textarea');
1229 if (!textarea) {
1230 throw new Error('Textarea not found');
1231 }
1232
1233 textarea.value = answer;
1234 const inputEvent = new Event('input', { bubbles: true });
1235 textarea.dispatchEvent(inputEvent);
1236 console.log('Answer inserted into textarea');
1237
1238 // Ждем немного для обновления UI
1239 await sleep(500);
1240
1241 // Находим и кликаем кнопку "Отправить ответ"
1242 const submitButton = Array.from(modal.querySelectorAll('button'))
1243 .find(btn => btn.textContent.includes('Отправить ответ'));
1244
1245 if (!submitButton) {
1246 throw new Error('Submit button not found');
1247 }
1248
1249 submitButton.click();
1250 console.log('Submit button clicked');
1251
1252 // Ждем немного после отправки
1253 await sleep(1000);
1254
1255 // Закрываем модальное окно через крестик
1256 const closeButton = modal.querySelector('button.ct3110-a1[aria-label="Крестик для закрытия"]');
1257 if (closeButton) {
1258 closeButton.click();
1259 console.log('Close button clicked');
1260 } else {
1261 console.log('Close button not found, trying alternative');
1262 // Альтернативный способ - клик по overlay
1263 const overlay = document.querySelector('.ct3110-a');
1264 if (overlay) {
1265 overlay.click();
1266 }
1267 }
1268
1269 // Ждем закрытия модального окна
1270 await waitForModalClose();
1271 console.log('Modal closed');
1272
1273 successCount++;
1274 console.log(`Answer ${successCount} sent successfully`);
1275
1276 } catch (error) {
1277 console.error(`Error sending answer for question ${i + 1}:`, error);
1278 // Закрываем модальное окно при ошибке
1279 const modal = document.querySelector('.ct3110-a');
1280 if (modal) {
1281 const closeButton = modal.querySelector('button.ct3110-a1[aria-label="Крестик для закрытия"]');
1282 if (closeButton) {
1283 closeButton.click();
1284 }
1285 }
1286 await sleep(500);
1287 }
1288 }
1289
1290 statusDiv.textContent = `Отправка завершена: ${successCount} из ${totalToSend} ответов отправлено`;
1291
1292 } catch (error) {
1293 console.error('Bulk answer error:', error);
1294 statusDiv.textContent = 'Ошибка при отправке ответов';
1295 } finally {
1296 answerAllBtn.disabled = false;
1297 }
1298 }
1299
1300 // Вспомогательная функция ожидания модального окна
1301 function waitForModal() {
1302 return new Promise((resolve) => {
1303 const checkModal = () => {
1304 const modal = document.querySelector('.ct3110-a');
1305 const textarea = modal?.querySelector('textarea');
1306 if (modal && textarea) {
1307 resolve();
1308 } else {
1309 setTimeout(checkModal, 100);
1310 }
1311 };
1312 checkModal();
1313 });
1314 }
1315
1316 // Вспомогательная функция ожидания закрытия модального окна
1317 function waitForModalClose() {
1318 return new Promise((resolve) => {
1319 const checkModal = () => {
1320 const modal = document.querySelector('.ct3110-a');
1321 if (!modal) {
1322 resolve();
1323 } else {
1324 setTimeout(checkModal, 100);
1325 }
1326 };
1327 setTimeout(checkModal, 500);
1328 });
1329 }
1330
1331 // Вспомогательная функция задержки
1332 function sleep(ms) {
1333 return new Promise(resolve => setTimeout(resolve, ms));
1334 }
1335
1336 // Функция для загрузки промпта из localStorage
1337 async function loadCustomPrompt() {
1338 try {
1339 const stored = localStorage.getItem('ozon_custom_prompt');
1340 if (stored) {
1341 customPrompt = stored;
1342 console.log('Custom prompt loaded');
1343 }
1344 } catch (error) {
1345 console.error('Error loading custom prompt:', error);
1346 }
1347 }
1348
1349 // Функция для сохранения промпта в localStorage
1350 function saveCustomPrompt(prompt) {
1351 try {
1352 localStorage.setItem('ozon_custom_prompt', prompt);
1353 customPrompt = prompt;
1354 console.log('Custom prompt saved');
1355 } catch (error) {
1356 console.error('Error saving custom prompt:', error);
1357 alert('Ошибка при сохранении промпта: ' + error.message);
1358 }
1359 }
1360
1361 // Функция для получения текущего промпта
1362 function getCurrentPrompt() {
1363 return customPrompt || DEFAULT_PROMPT;
1364 }
1365
1366 // Функция для показа модального окна с промптом
1367 function showPromptModal() {
1368 // Проверяем, не открыто ли уже модальное окно
1369 if (document.querySelector('.prompt-modal-overlay')) {
1370 return;
1371 }
1372
1373 const overlay = document.createElement('div');
1374 overlay.className = 'prompt-modal-overlay';
1375
1376 const modal = document.createElement('div');
1377 modal.className = 'prompt-modal';
1378
1379 const currentPrompt = getCurrentPrompt();
1380
1381 modal.innerHTML = `
1382 <div class="prompt-modal-header">
1383 <h3 class="prompt-modal-title">Редактирование промпта</h3>
1384 <button class="prompt-modal-close">✕</button>
1385 </div>
1386 <div class="prompt-modal-body">
1387 <div class="prompt-modal-hint">
1388 Используйте переменные: {productName}, {questionText}, {knowledgeBaseInfo}
1389 </div>
1390 <textarea class="prompt-modal-textarea">${currentPrompt}</textarea>
1391 </div>
1392 <div class="prompt-modal-footer">
1393 <button class="prompt-modal-reset">Сбросить на дефолтный</button>
1394 <div class="prompt-modal-actions">
1395 <button class="prompt-modal-cancel">Отмена</button>
1396 <button class="prompt-modal-save">Сохранить</button>
1397 </div>
1398 </div>
1399 `;
1400
1401 overlay.appendChild(modal);
1402 document.body.appendChild(overlay);
1403
1404 // Обработчики
1405 const closeBtn = modal.querySelector('.prompt-modal-close');
1406 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1407 const saveBtn = modal.querySelector('.prompt-modal-save');
1408 const resetBtn = modal.querySelector('.prompt-modal-reset');
1409 const textarea = modal.querySelector('.prompt-modal-textarea');
1410
1411 const closeModal = () => {
1412 overlay.remove();
1413 };
1414
1415 closeBtn.addEventListener('click', closeModal);
1416 cancelBtn.addEventListener('click', closeModal);
1417 overlay.addEventListener('click', (e) => {
1418 if (e.target === overlay) closeModal();
1419 });
1420
1421 saveBtn.addEventListener('click', () => {
1422 const newPrompt = textarea.value.trim();
1423 if (newPrompt) {
1424 saveCustomPrompt(newPrompt);
1425 alert('Промпт сохранен успешно!');
1426 closeModal();
1427 } else {
1428 alert('Промпт не может быть пустым');
1429 }
1430 });
1431
1432 resetBtn.addEventListener('click', () => {
1433 if (confirm('Вы уверены, что хотите сбросить промпт на дефолтный?')) {
1434 textarea.value = DEFAULT_PROMPT;
1435 localStorage.removeItem('ozon_custom_prompt');
1436 customPrompt = null;
1437 alert('Промпт сброшен на дефолтный');
1438 }
1439 });
1440 }
1441
1442 // Функция для показа модального окна выбора модели
1443 function showModelModal() {
1444 // Проверяем, не открыто ли уже модальное окно
1445 if (document.querySelector('.prompt-modal-overlay')) {
1446 return;
1447 }
1448
1449 // Загружаем настройки модели из localStorage
1450 loadModelSettings();
1451
1452 const overlay = document.createElement('div');
1453 overlay.className = 'prompt-modal-overlay';
1454
1455 const modal = document.createElement('div');
1456 modal.className = 'prompt-modal';
1457
1458 modal.innerHTML = `
1459 <div class="prompt-modal-header">
1460 <h3 class="prompt-modal-title">Настройки AI модели</h3>
1461 <button class="prompt-modal-close">✕</button>
1462 </div>
1463 <div class="prompt-modal-body">
1464 <div class="model-modal-form-group">
1465 <label class="model-modal-label">Провайдер:</label>
1466 <select class="model-modal-select" id="model-provider-select">
1467 <option value="rmcall" ${modelSettings.provider === 'rmcall' ? 'selected' : ''}>RM Call (стандартный)</option>
1468 <option value="openrouter" ${modelSettings.provider === 'openrouter' ? 'selected' : ''}>OpenRouter</option>
1469 </select>
1470 </div>
1471
1472 <div class="model-modal-form-group" id="model-select-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1473 <label class="model-modal-label">
1474 Модель:
1475 <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;">
1476 🔍 Проверить модели
1477 </button>
1478 </label>
1479 <select class="model-modal-select" id="model-name-select" size="8" style="height: auto;">
1480 ${getModelOptions()}
1481 </select>
1482 <div id="model-test-status" style="margin-top: 8px; font-size: 12px; color: #666;"></div>
1483 <div style="margin-top: 12px; display: flex; gap: 8px;">
1484 <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;" />
1485 <button id="add-model-btn" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">➕ Добавить</button>
1486 <button id="remove-model-btn" style="padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">🗑️ Удалить</button>
1487 </div>
1488 </div>
1489
1490 <div class="model-modal-form-group" id="api-key-group" style="display: ${modelSettings.provider === 'openrouter' ? 'block' : 'none'};">
1491 <label class="model-modal-label">API ключ OpenRouter:</label>
1492 <input type="password" class="model-modal-input" id="api-key-input" placeholder="Введите API ключ" value="${modelSettings.apiKey || ''}" />
1493 <div class="prompt-modal-hint" style="margin-top: 8px;">
1494 Получите бесплатный API ключ на <a href="https://openrouter.ai/keys" target="_blank" style="color: #17a2b8;">openrouter.ai/keys</a>
1495 </div>
1496 </div>
1497 </div>
1498 <div class="prompt-modal-footer">
1499 <div></div>
1500 <div class="prompt-modal-actions">
1501 <button class="prompt-modal-cancel">Отмена</button>
1502 <button class="prompt-modal-save">Сохранить</button>
1503 </div>
1504 </div>
1505 `;
1506
1507 overlay.appendChild(modal);
1508 document.body.appendChild(overlay);
1509
1510 // Обработчики
1511 const closeBtn = modal.querySelector('.prompt-modal-close');
1512 const cancelBtn = modal.querySelector('.prompt-modal-cancel');
1513 const saveBtn = modal.querySelector('.prompt-modal-save');
1514 const providerSelect = modal.querySelector('#model-provider-select');
1515 const modelNameSelect = modal.querySelector('#model-name-select');
1516 const apiKeyInput = modal.querySelector('#api-key-input');
1517 const modelSelectGroup = modal.querySelector('#model-select-group');
1518 const apiKeyGroup = modal.querySelector('#api-key-group');
1519 const testModelsBtn = modal.querySelector('#test-models-btn');
1520
1521 // Устанавливаем текущую модель
1522 if (modelSettings.model) {
1523 modelNameSelect.value = modelSettings.model;
1524 }
1525
1526 const closeModal = () => {
1527 overlay.remove();
1528 };
1529
1530 // Обработчик изменения провайдера
1531 providerSelect.addEventListener('change', () => {
1532 const isOpenRouter = providerSelect.value === 'openrouter';
1533 modelSelectGroup.style.display = isOpenRouter ? 'block' : 'none';
1534 apiKeyGroup.style.display = isOpenRouter ? 'block' : 'none';
1535 });
1536
1537 // Обработчик проверки моделей
1538 testModelsBtn.addEventListener('click', async () => {
1539 await testAllModels(modelNameSelect, apiKeyInput.value.trim());
1540 });
1541
1542 // Обработчик добавления модели
1543 const addModelBtn = modal.querySelector('#add-model-btn');
1544 const removeModelBtn = modal.querySelector('#remove-model-btn');
1545 const customModelInput = modal.querySelector('#custom-model-input');
1546
1547 addModelBtn.addEventListener('click', () => {
1548 const modelId = customModelInput.value.trim();
1549 if (!modelId) {
1550 alert('Пожалуйста, введите ID модели');
1551 return;
1552 }
1553
1554 // Проверяем, не добавлена ли уже эта модель
1555 if (!modelSettings.customModels) {
1556 modelSettings.customModels = [];
1557 }
1558
1559 if (modelSettings.customModels.includes(modelId)) {
1560 alert('Эта модель уже добавлена');
1561 return;
1562 }
1563
1564 // Добавляем модель
1565 modelSettings.customModels.push(modelId);
1566 saveModelSettings();
1567
1568 // Обновляем список моделей
1569 modelNameSelect.innerHTML = getModelOptions();
1570 modelNameSelect.value = modelId;
1571
1572 customModelInput.value = '';
1573 alert('Модель добавлена успешно!');
1574 });
1575
1576 removeModelBtn.addEventListener('click', () => {
1577 const selectedModel = modelNameSelect.value;
1578 if (!selectedModel) {
1579 alert('Пожалуйста, выберите модель для удаления');
1580 return;
1581 }
1582
1583 // Проверяем, является ли модель кастомной
1584 if (!modelSettings.customModels || !modelSettings.customModels.includes(selectedModel)) {
1585 alert('Можно удалять только добавленные вами модели');
1586 return;
1587 }
1588
1589 if (!confirm(`Вы уверены, что хотите удалить модель "${selectedModel}"?`)) {
1590 return;
1591 }
1592
1593 // Удаляем модель
1594 modelSettings.customModels = modelSettings.customModels.filter(m => m !== selectedModel);
1595 saveModelSettings();
1596
1597 // Обновляем список моделей
1598 modelNameSelect.innerHTML = getModelOptions();
1599
1600 alert('Модель удалена успешно!');
1601 });
1602
1603 closeBtn.addEventListener('click', closeModal);
1604 cancelBtn.addEventListener('click', closeModal);
1605 overlay.addEventListener('click', (e) => {
1606 if (e.target === overlay) closeModal();
1607 });
1608
1609 saveBtn.addEventListener('click', () => {
1610 const provider = providerSelect.value;
1611 const model = modelNameSelect.value;
1612 const apiKey = apiKeyInput.value.trim();
1613
1614 // Валидация
1615 if (provider === 'openrouter' && !apiKey) {
1616 alert('Пожалуйста, введите API ключ для OpenRouter');
1617 return;
1618 }
1619
1620 // Сохраняем настройки
1621 modelSettings.provider = provider;
1622 modelSettings.model = model;
1623 modelSettings.apiKey = apiKey;
1624
1625 saveModelSettings();
1626 alert('Настройки модели сохранены успешно!');
1627 closeModal();
1628 });
1629 }
1630
1631 // Функция для тестирования всех моделей
1632 async function testAllModels(selectElement, apiKey) {
1633 if (!apiKey) {
1634 alert('Пожалуйста, введите API ключ для проверки моделей');
1635 return;
1636 }
1637
1638 const statusDiv = document.querySelector('#model-test-status');
1639 const testBtn = document.querySelector('#test-models-btn');
1640
1641 testBtn.disabled = true;
1642 testBtn.textContent = '⏳ Проверка...';
1643 statusDiv.textContent = 'Проверка моделей...';
1644
1645 const models = [];
1646 for (let i = 0; i < selectElement.options.length; i++) {
1647 models.push({
1648 value: selectElement.options[i].value,
1649 text: selectElement.options[i].text,
1650 option: selectElement.options[i]
1651 });
1652 }
1653
1654 let workingCount = 0;
1655 let failedCount = 0;
1656
1657 for (let i = 0; i < models.length; i++) {
1658 const model = models[i];
1659 statusDiv.textContent = `Проверка ${i + 1} из ${models.length}: ${model.text}`;
1660
1661 try {
1662 const isWorking = await testModel(model.value, apiKey);
1663
1664 if (isWorking) {
1665 model.option.text = '✅ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1666 workingCount++;
1667 } else {
1668 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1669 failedCount++;
1670 }
1671 } catch (error) {
1672 console.error(`Model ${model.value} test failed:`, error);
1673 model.option.text = '❌ ' + model.text.replace('✅ ', '').replace('❌ ', '');
1674 failedCount++;
1675 }
1676
1677 // Небольшая задержка между запросами
1678 await sleep(500);
1679 }
1680
1681 testBtn.disabled = false;
1682 testBtn.textContent = '🔍 Проверить модели';
1683 statusDiv.innerHTML = `<span style="color: #28a745;">✅ Работает: ${workingCount}</span> | <span style="color: #dc3545;">❌ Не работает: ${failedCount}</span>`;
1684 }
1685
1686 // Функция для тестирования одной модели
1687 async function testModel(modelName, apiKey) {
1688 try {
1689 const response = await GM.xmlhttpRequest({
1690 method: 'POST',
1691 url: 'https://openrouter.ai/api/v1/chat/completions',
1692 headers: {
1693 'Content-Type': 'application/json',
1694 'Authorization': `Bearer ${apiKey}`,
1695 'HTTP-Referer': window.location.href,
1696 'X-Title': 'Ozon AI Answer Generator'
1697 },
1698 data: JSON.stringify({
1699 model: modelName,
1700 messages: [
1701 {
1702 role: 'user',
1703 content: 'Ответь одним словом: привет'
1704 }
1705 ],
1706 max_tokens: 10
1707 }),
1708 timeout: 10000
1709 });
1710
1711 if (response.status === 200) {
1712 const data = JSON.parse(response.responseText);
1713 return !!(data.choices && data.choices[0] && data.choices[0].message);
1714 }
1715
1716 return false;
1717 } catch (error) {
1718 console.error(`Model ${modelName} test failed:`, error);
1719 return false;
1720 }
1721 }
1722
1723 // Функция для загрузки настроек модели из localStorage
1724 function loadModelSettings() {
1725 try {
1726 const stored = localStorage.getItem('ozon_model_settings');
1727 if (stored) {
1728 const settings = JSON.parse(stored);
1729 modelSettings.provider = settings.provider || 'rmcall';
1730 modelSettings.model = settings.model || 'google/gemini-2.0-flash-exp:free';
1731 modelSettings.apiKey = settings.apiKey || '';
1732 modelSettings.customModels = settings.customModels || [];
1733 console.log('Model settings loaded:', modelSettings);
1734 }
1735 } catch (error) {
1736 console.error('Error loading model settings:', error);
1737 }
1738 }
1739
1740 // Функция для генерации списка моделей
1741 function getModelOptions() {
1742 const defaultModels = [
1743 { value: 'arcee-ai/trinity-large-preview:free', label: 'Arcee Trinity Large (бесплатно)' },
1744 { value: 'openrouter/free', label: 'OpenRouter Free' }
1745 ];
1746
1747 let options = '';
1748
1749 // Добавляем дефолтные модели
1750 for (const model of defaultModels) {
1751 options += `<option value="${model.value}">${model.label}</option>`;
1752 }
1753
1754 // Добавляем кастомные модели
1755 if (modelSettings.customModels && modelSettings.customModels.length > 0) {
1756 options += '<option disabled>──────────</option>';
1757 for (const modelId of modelSettings.customModels) {
1758 options += `<option value="${modelId}">🔧 ${modelId}</option>`;
1759 }
1760 }
1761
1762 return options;
1763 }
1764
1765 // Функция для сохранения настроек модели в localStorage
1766 function saveModelSettings() {
1767 try {
1768 localStorage.setItem('ozon_model_settings', JSON.stringify(modelSettings));
1769 console.log('Model settings saved:', modelSettings);
1770 } catch (error) {
1771 console.error('Error saving model settings:', error);
1772 alert('Ошибка при сохранении настроек модели: ' + error.message);
1773 }
1774 }
1775
1776 // Универсальная функция для вызова AI
1777 async function callAI(prompt) {
1778 // Загружаем настройки модели
1779 loadModelSettings();
1780
1781 console.log('Using AI provider:', modelSettings.provider);
1782 console.log('Prompt being sent to AI:', prompt);
1783
1784 if (modelSettings.provider === 'openrouter') {
1785 // Используем OpenRouter API
1786 if (!modelSettings.apiKey) {
1787 throw new Error('API ключ OpenRouter не настроен. Откройте настройки модели и введите API ключ.');
1788 }
1789
1790 console.log('Calling OpenRouter API with model:', modelSettings.model);
1791
1792 try {
1793 const response = await GM.xmlhttpRequest({
1794 method: 'POST',
1795 url: 'https://openrouter.ai/api/v1/chat/completions',
1796 headers: {
1797 'Content-Type': 'application/json',
1798 'Authorization': `Bearer ${modelSettings.apiKey}`,
1799 'HTTP-Referer': window.location.href,
1800 'X-Title': 'Ozon AI Answer Generator'
1801 },
1802 data: JSON.stringify({
1803 model: modelSettings.model,
1804 messages: [
1805 {
1806 role: 'user',
1807 content: prompt
1808 }
1809 ]
1810 })
1811 });
1812
1813 console.log('OpenRouter response status:', response.status);
1814 console.log('OpenRouter response:', response.responseText);
1815
1816 if (response.status !== 200) {
1817 console.error('OpenRouter API error:', response);
1818 throw new Error(`OpenRouter API error: ${response.status} - ${response.statusText}`);
1819 }
1820
1821 const data = JSON.parse(response.responseText);
1822
1823 if (!data.choices || !data.choices[0] || !data.choices[0].message) {
1824 console.error('Invalid OpenRouter response:', data);
1825 throw new Error('Неверный формат ответа от OpenRouter API');
1826 }
1827
1828 return data.choices[0].message.content;
1829
1830 } catch (error) {
1831 console.error('OpenRouter API call failed:', error);
1832 throw new Error(`Ошибка вызова OpenRouter API: ${error.message}`);
1833 }
1834 } else {
1835 // Используем стандартный RM.aiCall
1836 console.log('Calling RM.aiCall');
1837 return await RM.aiCall(prompt);
1838 }
1839 }
1840
1841 // Инициализация массовой генерации
1842 function initBulkGeneration() {
1843 // Проверяем, что мы на странице со списком вопросов
1844 if (!window.location.href.includes('/app/reviews/questions')) {
1845 return;
1846 }
1847
1848 console.log('Initializing bulk generation...');
1849
1850 addBulkGenerationStyles();
1851
1852 // Ждем загрузки таблицы
1853 const observer = new MutationObserver(debounce(() => {
1854 const tableContainer = document.querySelector('.n1d-aup3');
1855 if (tableContainer && !document.querySelector('.bulk-generation-panel')) {
1856 createBulkGenerationPanel();
1857 }
1858 }, 300));
1859
1860 observer.observe(document.body, {
1861 childList: true,
1862 subtree: true
1863 });
1864
1865 // Пробуем создать панель сразу
1866 const tableContainer = document.querySelector('.n1d-aup3');
1867 if (tableContainer) {
1868 createBulkGenerationPanel();
1869 }
1870 }
1871
1872 // Запускаем инициализацию массовой генерации
1873 initBulkGeneration();
1874
1875})();