Ozon AI Answer Generator

AI-powered answer generator for Ozon seller questions

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})();