Wildberries Description Generator 3.1

Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов

Size

168.8 KB

Version

3.1.28

Created

Jan 26, 2026

Updated

7 days ago

1// ==UserScript==
2// @name		Wildberries Description Generator 3.1
3// @description		Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
4// @version		3.1.28
5// @match		https://*.seller.wildberries.ru/*
6// @icon		https://static-basket-02.wbbasket.ru/vol20/root-monorepo/latest/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('Wildberries Description Generator 3.0: Расширение запущено');
12
13    // ============================================
14    // УТИЛИТЫ
15    // ============================================
16
17    function debounce(func, wait) {
18        let timeout;
19        return function executedFunction(...args) {
20            clearTimeout(timeout);
21            timeout = setTimeout(() => func(...args), wait);
22        };
23    }
24
25    function formatNumber(num) {
26        if (num >= 1000) {
27            return Math.round(num / 1000) + 'K';
28        }
29        return num.toString();
30    }
31
32    // ============================================
33    // AI PROMPT GENERATORS
34    // ============================================
35
36    function generateMasksPrompt(productInfo) {
37        const releaseFormSection = productInfo.releaseForm ? `• Форма выпуска: ${productInfo.releaseForm}\n` : '';
38        
39        return `Ты — эксперт по SEO на Wildberries. Предложи поисковые маски и ключевые слова ТОЛЬКО ДЛЯ КОНКРЕТНОГО ТОВАРА, строго основываясь на предоставленных данных.
40
41ДАННЫЕ О ТОВАРЕ:
42• Название: ${productInfo.title || 'не указано'}
43${releaseFormSection}• Состав: ${productInfo.composition || 'не указан'}
44
45ЗАДАЧА:
46Предложи 2 типа запросов, строго соответствующих типу и форме выпуска данного товара:
47
48МАСКИ (15 штук) — короткие слова для поиска в аналитике WB:
49
501-2 слова максимум
51
52Используй как отдельные слова, так и с предлогами
53
54Пример: для товара «Витамины в таблетках» → "витамины", "витамины для".
55
56Пример: для товара «Крем для лица» → "крем", "крем для".
57
58КЛЮЧЕВЫЕ СЛОВА (15 штук) — точные запросы покупателей:
59
602-4 слова
61
62Пример: "витамины для глаз в таблетках", "крем для лица увлажняющий".
63
64ЯВНОЕ СООТВЕТСТВИЕ ТОВАРУ (ГЛАВНОЕ ПРАВИЛО):
65
66Определи тип товара и форму выпуска по названию${productInfo.releaseForm ? ' и указанной форме выпуска' : ''}. Если в названии например есть слова «витамины», «таблетки», «капсулы» — это БАД/витамины. Если например «крем», «сыворотка», «лосьон» — это косметика/средство для ухода. Не смешивай категории.
67
68Запрещено предлагать запросы для других форм выпуска или типов товаров. Например, усли товар — витамины или таблетки, НЕЛЬЗЯ предлагать «капли для глаз», «гель для век», «маска для глаз», «раствор».
69
70Запрещено расширять назначение товара. Используй только те проблемы и аудиторию, которые явно следуют из названия и состава (например, «для глаз», «для зрения» из названия «Витамины для глаз»).
71
72КАТЕГОРИИ ДЛЯ МАСОК (адаптируй под тип товара):
73
74Тип/форма товара (3 маски): Используй слова из названия${productInfo.releaseForm ? ' и формы выпуска' : ''} (витамины, крем, сыворотка, таблетки, капсулы) и их синонимы (серум, бад, комплекс).
75
76Тип + предлог (5 масок): "[тип] для", "[тип] с", "[тип] от", "[тип] против", "[тип] против".
77
78Назначение из названия (3 маски): Ключевая проблема/зона из названия ("для глаз", "для зрения", "увлажнение", "от морщин").
79
80Ингредиенты ИЗ СОСТАВА (3 маски): Только основные компоненты из ${productInfo.composition}.
81
82Общий эффект (1 маска): "здоровье", "омоложение", "укрепление".
83
84КАТЕГОРИИ ДЛЯ КЛЮЧЕВЫХ СЛОВ (адаптируй под тип товара):
85
86Товар + назначение (6 ключей): "[тип] для [проблемы/зоны]". Основная категория.
87
88Товар + ингредиент ИЗ СОСТАВА (4 ключа): "[тип] с [компонентом]".
89
90Товар + аудитория (3 ключа): Только если явно следует из названия ("для мужчин", "для женщин после 40").
91
92Товар + эффект/свойство (2 ключа): "[тип] [эффект]".
93
94СТРОГИЕ ПРАВИЛА ГЕНЕРАЦИИ:
95
96Русский язык (кроме spf, ph).
97
98НЕ бренды и НЕ конкуренты.
99
100Все слова уникальные, без повторов.
101
102Ингредиенты — ТОЛЬКО ИЗ СОСТАВА.
103
104Форма выпуска — ТОЛЬКО ИЗ НАЗВАНИЯ${productInfo.releaseForm ? ' И УКАЗАННОЙ ФОРМЫ ВЫПУСКА' : ''}. Не придумывай другие формы.
105
106Не более 1 предлога в маске ("для глаз", а не "витамины для для глаз").
107
108ФИНАЛЬНАЯ ПРОВЕРКА И ФИЛЬТРАЦИЯ:
109Перед выдачей JSON проверь каждый элемент в списках masks и keywords и удали те, которые:
110
111Соответствуют другому типу товара/форме выпуска (например, для витаминов: "капли", "гель", "спрей").
112
113Содержат названия брендов, странные комбинации слов ("окоспас") или ингредиенты не из состава.
114
115Слишком общие и не отражают суть товара ("для людей", "для здоровья" — без уточнения).
116
117Противоречат данным (например, товар "для глаз", а ключ "для суставов").
118
119ФОРМАТ ОТВЕТА:
120Верни ТОЛЬКО JSON без каких-либо пояснений.
121
122{
123"masks": ["маска1", "маска2", ...],
124"keywords": ["ключ1", "ключ2", ...]
125}
126
127Начни ответ сразу с {`;
128    }
129
130    function generateProductAnalysisPrompt(productInfo, keywords) {
131        const releaseFormSection = productInfo.releaseForm ? `• Форма выпуска: ${productInfo.releaseForm}\n` : '';
132        
133        return `Проанализируй товар и создай критерии для умной фильтрации поисковых запросов из аналитики Wildberries.
134
135ДАННЫЕ О ТОВАРЕ:
136• Название: ${productInfo.title || 'не указано'}
137${releaseFormSection}• Состав: ${productInfo.composition || 'не указан'}
138• Базовые ключи: ${keywords.join(', ')}
139
140ЗАДАЧА:
141Создай критерии фильтрации, чтобы при сборе данных из аналитики мы НЕ ПОТЕРЯЛИ релевантные запросы.
142
143ОПРЕДЕЛИ:
144
1451. КАТЕГОРИЯ ТОВАРА (одна из):
146   - косметика_лицо (кремы, сыворотки, маски для лица)
147   - косметика_волосы (шампуни, маски, масла для волос)
148   - косметика_тело (кремы для тела, скрабы, масла для тела)
149   - бад_витамины (витамины, минералы, БАДы)
150   - бад_спорт (спортивное питание, протеины)
151   - другое
152
1532. ЦЕЛЕВАЯ АУДИТОРИЯ:
154   - для_мужчин / для_женщин / для_детей / универсальный
155
1563. НАЗНАЧЕНИЕ (основное применение):
157   - Например: "увлажнение кожи лица", "рост волос", "повышение иммунитета"
158
1594. КЛЮЧЕВЫЕ КОМПОНЕНТЫ (из состава):
160   - Список главных активных компонентов
161
1625. РАЗРЕШЕННЫЕ АНГЛИЙСКИЕ СЛОВА/БРЕНДЫ:
163   - Если в названии есть английские слова (например, "Elementary"), их НУЖНО разрешить
164   - Список слов, которые можно оставлять в запросах
165
1666. ИСКЛЮЧАЕМЫЕ НАЗНАЧЕНИЯ:
167   - Список назначений, которые НЕ подходят для этого товара
168   - Например, для "сыворотки для лица" исключить: "сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела".
169   - ВАЖНО: Оставляй общие запросы без уточнения назначения (например, "сыворотка", "витамины")
170   - Слова не имеющие общепонятного значения: неофарм, урофарм, либридерм и другие. 
171
172ПРИМЕР:
173
174Товар: "Elementary Сыворотка для лица с витамином С"
175Форма выпуска: "Сыворотка"
176Состав: "Аскорбиновая кислота, Ниацинамид, Гиалуроновая кислота"
177
178ПРАВИЛЬНЫЙ ОТВЕТ:
179{
180  "category": "косметика_лицо",
181  "target_audience": "универсальный",
182  "purpose": "увлажнение и осветление кожи лица",
183  "key_components": ["витамин с", "аскорбиновая кислота", "ниацинамид", "гиалуроновая кислота"],
184  "allowed_english_words": ["elementary"],
185  "excluded_purposes": ["сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела"]
186}
187
188Верни ТОЛЬКО JSON в формате выше. НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
189    }
190
191    function generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity = {}, customPrompt = '') {
192        // Фильтруем запросы по минимальной популярности (минимум 1000 запросов в месяц)
193        const MIN_POPULARITY = 1000;
194        const popularQueries = filteredQueries.filter(q => {
195            const popularity = queryPopularity[q.toLowerCase()] || 0;
196            return popularity >= MIN_POPULARITY;
197        });
198        
199        // Если после фильтрации осталось мало запросов, используем все
200        const queriesToUse = popularQueries.length >= 20 ? popularQueries : filteredQueries;
201        
202        const sortedQueries = queriesToUse
203            .map(q => ({ query: q, pop: queryPopularity[q.toLowerCase()] || 0 }))
204            .sort((a, b) => b.pop - a.pop);
205        
206        // Оптимизированное распределение: всего 40 запросов вместо 60
207        const highPriority = sortedQueries.slice(0, 15).map(q => q.query);
208        const mediumPriority = sortedQueries.slice(15, 30).map(q => q.query);
209        const lowPriority = sortedQueries.slice(30, 40).map(q => q.query);
210
211        const customPromptSection = customPrompt ? `ДОПОЛНИТЕЛЬНЫЕ УКАЗАНИЯ ОТ ПОЛЬЗОВАТЕЛЯ:\n${customPrompt}\n\n` : '';
212
213        return `Создай SEO-описание товара для Wildberries.
214
215ДАННЫЕ:
216• Название: ${productInfo.title || 'не указано'}
217• Состав: ${productInfo.composition || 'не указан'}
218• Базовые ключи: ${keywords.join(', ')}
219
220${customPromptSection}ЗАПРОСЫ ПО ПРИОРИТЕТУ (отобраны самые популярные):
221
222🔴 ВЫСОКИЙ ПРИОРИТЕТ (использовать ВСЕ, минимум 1 раз каждый):
223${highPriority.map((q, i) => `${i+1}. "${q}"`).join('\n')}
224
225🟡 СРЕДНИЙ ПРИОРИТЕТ (использовать 60-80%):
226${mediumPriority.length > 0 ? mediumPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
227
228🟢 НИЗКИЙ ПРИОРИТЕТ (использовать по возможности):
229${lowPriority.length > 0 ? lowPriority.map((q, i) => `${i+1}. "${q}"`).join('\n') : 'нет запросов'}
230
231СТРОГАЯ ПРЕ-ФИЛЬТРАЦИЯ ЗАПРОСОВ (ВЫПОЛНИ ПЕРВЫМ ШАГОМ):
232
233ПЕРЕД НАЧАЛОМ ГЕНЕРАЦИИ ТЕКСТА, ПРОАНАЛИЗИРУЙ И ОТФИЛЬТРУЙ ВСЕ ЗАПРОСЫ:
234
235Фильтрация брендов и конкурентов: Выяви и ВЫЧЕРКНИ ИЗ РАССМОТРЕНИЯ ЛЮБЫЕ запросы, содержащие:
236
237Прямые названия брендов, компаний или линеек (Nivea, Garnier, L'Oreal).
238
239Слова «аналог», «как у», «похож на», «лучше чем» с указанием или намеком на другой продукт.
240
241Любые сравнения с другими средствами.
242
243Фильтрация отсутствующих компонентов (КРИТИЧЕСКИ ВАЖНО): 
244
245ПОЛНЫЙ СПИСОК КОМПОНЕНТОВ В СОСТАВЕ: ${productInfo.composition}
246
247Сравни КАЖДЫЙ запрос с этим списком. ВЫЧЕРКНИ запросы, которые упоминают компоненты (витамины, кислоты, экстракты, масла, минералы), которых НЕТ в составе выше.
248
249Примеры компонентов для проверки: лютеин, зеаксантин, коллаген, кератин, ретинол, 
250гиалуроновая кислота, витамин С, витамин Е, витамин А, цинк, селен, омега-3, коэнзим Q10, биотин и т.д.
251
252Если в запросе есть "витамины с лютеином", а лютеина НЕТ в составе — ВЫЧЕРКНИ весь запрос.
253
254Если в запросе есть "крем с гиалуроновой кислотой", а гиалуроновой кислоты НЕТ в составе — ВЫЧЕРКНИ весь запрос.
255
256ИСКЛЮЧЕНИЕ: Общие запросы без упоминания конкретных компонентов (например, "витамины для глаз", "крем для лица") — ОСТАВЛЯЙ.
257
258Фильтрация непонятных и составных слов: ВЫЧЕРКНИ запросы, содержащие слова или словосочетания, которые:
259
260Являются очевидной комбинацией двух или более слов (слитной или через дефис) и выглядят как возможное уникальное название продукта, бренда или маркетинговый термин (например: "окоспас", "здравушка", "быстросупер", "био-лайт", "аквафлекс").
261
262Вызывают сомнение в их общеупотребительности или наличии четкого значения в контексте товара.
263
264Не являются стандартным термином из области косметики, ухода, питания и т.д.
265
266Работай только с оставшимися запросами. Приоритет правильности информации ВЫШЕ приоритета вхождения всех ключей. Если после фильтрации высокоприоритетных запросов не осталось — используй те, что прошли проверку, из среднего и низкого приоритета.
267
268ТРЕБОВАНИЯ К ОПИСАНИЮ:
269
270Объем 3500–4000 символов, только текст.
271
272Структура:
273
274Введение (400-500 симв.) — суть товара, что это, зачем, для кого, что делает кратко.
275
276Проблема и решение (600-800 симв.) — основная проблема, которую решает это средство, к чему приводит проблема. Кратко как этот продукт решает проблему.
277
278Состав и действие (800-1000 симв.) — ингредиенты и их польза. Подробная информация о составе и компонентах которые усиливают друг друга. как они работают, зачем они нужны. ИСПОЛЬЗУЙ ТОЛЬКО КОМПОНЕНТЫ ИЗ ПРЕДОСТАВЛЕННОГО СОСТАВА.
279
280Применение (600-800 симв.) — для кого, как работает
281
282Отзывы покупателей (300-400 симв.) — отдельный абзац, который начинается фразой "Наши покупатели отмечают ... через ...", где описан эффект и сроки появления результата
283
284Заключение (400-500 симв.) — результат для покупателя. что получит покупатель при использовании средства.
285
286Плавные переходы между частями, без заголовков.
287
288Все базовые ключи — минимум 1 раз.
289
290Каждый запрос использовать не более 2 раз.
291
292АБСОЛЮТНАЯ КОРРЕКТНОСТЬ (КРИТИЧЕСКИ ВАЖНО): 
293
294Запрещено упоминать компоненты, которых нет в предоставленном составе: ${productInfo.composition}
295
296НЕ ПРИДУМЫВАЙ компоненты. НЕ ДОБАВЛЯЙ компоненты из запросов, если их нет в составе.
297
298Если отфильтрованный запрос содержит компонент, которого нет в составе — ПРОПУСТИ ЕГО ПОЛНОСТЬЮ.
299
300Все компоненты состава — 1–2 раза, без списков, вплетай в текст.
301
302Размер текста минимум 3000 символов - ПРОВЕРЬ!
303
304ГРУППИРОВКА ЗАПРОСОВ:
3059) Сгруппируй отфильтрованные запросы по смыслу:
306• Тип/категория товара
307• Проблемы/назначение
308• Состав/ингредиенты
309• Аудитория
310• Эффект/результат
31110) В каждом абзаце используй запросы только из 1–2 групп.
31211) Не смешивай в одном абзаце «аудиторию», «состав» и «эффект» одновременно.
313
314АНТИСПАМ-ПРАВИЛА:
31512) В каждом абзаце максимум 2 предложения с ключевыми запросами подряд.
31613) Между предложениями с ключами — минимум 1 предложение без ключей.
31714) Не более 20% предложений начинаются с ключевого запроса.
31815) Встраивай ключи в середину предложений, не начинай ими фразы.
31916) Плотность ключей: не более 3 запросов на 100 слов.
32017) Одно и то же ключевое словосочетание — не чаще 1 раза на абзац.
32118) Не ставь два длинных ключа (длиннее 4 слов) в одном предложении.
32219) Чередуй длину предложений: 20–25% короткие (8–12 слов), остальные средние.
323
324СТИЛЬ:
32520) Не повторяй связки «подходит для», «помогает», «обеспечивает» более 2 раз подряд.
32621) Не начинай более 2 предложений подряд с одного слова.
32722) Информативно, конкретные факты, без воды.
328
329ЗАПРЕТЫ:
33023) Только русский язык. Категорически запрещено использовать слова и фразы, отфильтрованные на этапе пре-фильтрации.
33124) Запрещены слова: «лекарство», «препарат», «революционный», «инновационный», «уникальный».
33225) Без заголовков, списков, вопросов, эмоджи, инструкций хранения и описания неактивных компонентов.
33326) Никаких упоминаний брендов, аналогов, сравнений или непонятных составных терминов.
33427) ЗАПРЕЩЕНО упоминать компоненты, которых нет в составе: ${productInfo.composition}. Если запрос содержит такой компонент — НЕ ИСПОЛЬЗУЙ этот запрос.
335
336ПРИМЕР ХОРОШЕГО АБЗАЦА (НЕ КОПИРУЙ ДОСЛОВНО, ТОЛЬКО СТИЛЬ):
337Средство с мягкой очищающей основой поддерживает комфорт кожи головы и помогает дольше сохранять ощущение свежести. В ежедневном уходе особенно ценится формула, где бессульфатный шампунь для жирных волос работает деликатно и без перегруза. Благодаря сочетанию цинка и гидролизата протеинов пшеницы волосы выглядят более ухоженными, а кожа головы — более сбалансированной.
338
339ПРИМЕР ПЛОХОГО АБЗАЦА (НЕ ДЕЛАЙ ТАК):
340Шампунь для жирных волос у корней очищает. Шампунь для жирной кожи головы помогает. Шампунь для волос женский укрепляет.
341
342ФИНАЛЬНАЯ ПРОВЕРКА ПЕРЕД ВЫВОДОМ:
343Перед тем как вернуть описание, проверь ЕЩЕ РАЗ:
3441. Все ли упомянутые компоненты есть в составе: ${productInfo.composition}
3452. Если ты упомянул компонент, которого нет в составе — УДАЛИ это предложение
3463. Перепиши предложения без упоминания отсутствующих компонентов
347
348ВЫВОД:
349Начни сразу с описания. Никаких вступлений, вопросов и пояснений. Используй только проверенные и отфильтрованные запросы, корректные данные о составе и названии.
350
351`;
352    }
353
354    // ============================================
355    // СТИЛИ
356    // ============================================
357
358    TM_addStyle(`
359        .wb-desc-modal {
360            position: fixed;
361            top: 0;
362            left: 0;
363            width: 100%;
364            height: 100%;
365            background: rgba(0, 0, 0, 0.5);
366            display: flex;
367            align-items: center;
368            justify-content: center;
369            z-index: 10000;
370        }
371        
372        .wb-desc-modal-content {
373            background: white;
374            border-radius: 12px;
375            padding: 24px;
376            max-width: 700px;
377            width: 90%;
378            max-height: 85vh;
379            overflow-y: auto;
380            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
381        }
382        
383        .wb-desc-modal-header {
384            font-size: 22px;
385            font-weight: 600;
386            margin-bottom: 20px;
387            color: #001a34;
388        }
389        
390        .wb-desc-input-group {
391            margin-bottom: 16px;
392        }
393        
394        .wb-desc-label {
395            display: block;
396            margin-bottom: 8px;
397            font-weight: 500;
398            color: #001a34;
399            font-size: 16px;
400        }
401        
402        .wb-desc-textarea {
403            width: 100%;
404            min-height: 100px;
405            padding: 12px;
406            border: 1px solid #d1d5db;
407            border-radius: 8px;
408            font-size: 16px;
409            font-family: inherit;
410            resize: vertical;
411            box-sizing: border-box;
412        }
413        
414        .wb-desc-result {
415            background: #f3f4f6;
416            padding: 16px;
417            border-radius: 8px;
418            margin-bottom: 16px;
419            max-height: 300px;
420            overflow-y: auto;
421            white-space: pre-wrap;
422            word-wrap: break-word;
423            font-size: 15px;
424            line-height: 1.6;
425        }
426        
427        .wb-desc-result-editable {
428            background: white;
429            padding: 16px;
430            border-radius: 8px;
431            margin-bottom: 16px;
432            max-height: 300px;
433            overflow-y: auto;
434            white-space: pre-wrap;
435            word-wrap: break-word;
436            font-size: 15px;
437            line-height: 1.6;
438            border: 2px solid #d1d5db;
439            outline: none;
440            transition: border-color 0.2s;
441        }
442        
443        .wb-desc-result-editable:focus {
444            border-color: #9333ea;
445            box-shadow: 0 0 0 3px rgba(147, 51, 234, 0.1);
446        }
447        
448        .wb-desc-english-word {
449            background: linear-gradient(135deg, #fef3c7, #fde68a);
450            padding: 2px 6px;
451            border-radius: 4px;
452            font-weight: 500;
453            color: #92400e;
454            border: 1px solid #fbbf24;
455        }
456        
457        .wb-desc-char-count {
458            text-align: right;
459            font-size: 14px;
460            color: #6b7280;
461            margin-top: 4px;
462        }
463        
464        .wb-desc-char-count.warning {
465            color: #f59e0b;
466        }
467        
468        .wb-desc-char-count.error {
469            color: #ef4444;
470        }
471        
472        .wb-desc-char-count.success {
473            color: #10b981;
474        }
475        
476        .wb-desc-buttons {
477            display: flex;
478            gap: 12px;
479            justify-content: flex-end;
480            margin-top: 20px;
481            flex-wrap: wrap;
482        }
483        
484        .wb-desc-btn {
485            padding: 10px 20px;
486            border: none;
487            border-radius: 8px;
488            font-size: 16px;
489            font-weight: 500;
490            cursor: pointer;
491            transition: all 0.2s;
492        }
493        
494        .wb-desc-btn-primary {
495            background: linear-gradient(135deg, #9333ea, #6366f1);
496            color: white;
497        }
498        
499        .wb-desc-btn-primary:hover {
500            background: linear-gradient(135deg, #7e22ce, #4f46e5);
501            transform: translateY(-1px);
502            box-shadow: 0 4px 12px rgba(147, 51, 234, 0.3);
503        }
504        
505        .wb-desc-btn-primary:disabled {
506            background: #9ca3af;
507            cursor: not-allowed;
508            transform: none;
509            box-shadow: none;
510        }
511        
512        .wb-desc-btn-secondary {
513            background: #e5e7eb;
514            color: #374151;
515        }
516        
517        .wb-desc-btn-secondary:hover {
518            background: #d1d5db;
519        }
520        
521        .wb-desc-btn-success {
522            background: linear-gradient(135deg, #10b981, #059669);
523            color: white;
524        }
525        
526        .wb-desc-btn-success:hover {
527            background: linear-gradient(135deg, #059669, #047857);
528            transform: translateY(-1px);
529            box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
530        }
531        
532        .wb-desc-generator-btn {
533            margin-left: 12px;
534            padding: 10px 20px;
535            background: linear-gradient(135deg, #9333ea, #6366f1);
536            color: white;
537            border: none;
538            border-radius: 8px;
539            font-size: 16px;
540            font-weight: 500;
541            cursor: pointer;
542            transition: all 0.2s;
543        }
544        
545        .wb-desc-generator-btn:hover {
546            background: linear-gradient(135deg, #7e22ce, #4f46e5);
547            transform: translateY(-2px);
548            box-shadow: 0 4px 12px rgba(147, 51, 234, 0.4);
549        }
550        
551        .wb-desc-status {
552            margin-top: 12px;
553            padding: 12px 16px;
554            border-radius: 8px;
555            font-size: 15px;
556        }
557        
558        .wb-desc-status.info {
559            background: #dbeafe;
560            color: #1e40af;
561            border-left: 4px solid #3b82f6;
562        }
563        
564        .wb-desc-status.success {
565            background: #d1fae5;
566            color: #065f46;
567            border-left: 4px solid #10b981;
568        }
569        
570        .wb-desc-status.error {
571            background: #fee2e2;
572            color: #991b1b;
573            border-left: 4px solid #ef4444;
574        }
575        
576        .wb-desc-suggest-btn {
577            margin-top: 8px;
578            padding: 8px 16px;
579            background: linear-gradient(135deg, #6366f1, #8b5cf6);
580            color: white;
581            border: none;
582            border-radius: 6px;
583            font-size: 15px;
584            font-weight: 500;
585            cursor: pointer;
586            transition: all 0.2s;
587        }
588        
589        .wb-desc-suggest-btn:hover {
590            background: linear-gradient(135deg, #4f46e5, #7c3aed);
591            transform: translateY(-1px);
592        }
593        
594        .wb-desc-suggest-btn:disabled {
595            background: #9ca3af;
596            cursor: not-allowed;
597            transform: none;
598        }
599        
600        .wb-desc-masks-container {
601            margin-top: 12px;
602            padding: 16px;
603            background: #f9fafb;
604            border-radius: 8px;
605            border: 1px solid #e5e7eb;
606        }
607        
608        .wb-desc-masks-header {
609            font-weight: 600;
610            margin-bottom: 12px;
611            color: #374151;
612            font-size: 15px;
613            display: flex;
614            justify-content: space-between;
615            align-items: center;
616            flex-wrap: wrap;
617            gap: 8px;
618        }
619        
620        .wb-desc-masks-group {
621            margin-bottom: 12px;
622        }
623        
624        .wb-desc-masks-group:last-child {
625            margin-bottom: 0;
626        }
627        
628        .wb-desc-masks-group-title {
629            font-size: 12px;
630            color: #6b7280;
631            margin-bottom: 8px;
632            text-transform: uppercase;
633            letter-spacing: 0.5px;
634        }
635        
636        .wb-desc-masks-grid {
637            display: flex;
638            flex-wrap: wrap;
639            gap: 8px;
640        }
641        
642        .wb-mask-chip {
643            display: inline-flex;
644            align-items: center;
645            gap: 6px;
646            padding: 8px 14px;
647            border-radius: 20px;
648            font-size: 14px;
649            cursor: pointer;
650            transition: all 0.2s;
651            border: 2px solid transparent;
652            user-select: none;
653        }
654        
655        .wb-mask-chip:hover {
656            transform: translateY(-2px);
657            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
658        }
659        
660        .wb-mask-chip.selected {
661            border-color: #10b981;
662            box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
663        }
664        
665        .wb-mask-chip[data-type="маска"] { background: #dbeafe; color: #1e40af; }
666        .wb-mask-chip[data-type="ключ"] { background: #fef3c7; color: #92400e; }
667        .wb-mask-chip[data-type="синоним"] { background: #fce7f3; color: #9d174d; }
668        .wb-mask-chip[data-type="назначение"] { background: #d1fae5; color: #065f46; }
669        .wb-mask-chip[data-type="зона"] { background: #fef3c7; color: #92400e; }
670        .wb-mask-chip[data-type="ингредиент"] { background: #ede9fe; color: #5b21b6; }
671        .wb-mask-chip[data-type="смежный"] { background: #f3f4f6; color: #374151; }
672        
673        .wb-desc-suggested-keywords {
674            margin-top: 12px;
675            padding: 12px;
676            background: #f9fafb;
677            border-radius: 8px;
678            border: 1px солид #e5e7eb;
679        }
680        
681        .wb-desc-suggested-header {
682            font-weight: 600;
683            margin-bottom: 8px;
684            color: #374151;
685            font-size: 15px;
686        }
687        
688        .wb-desc-checkbox-group {
689            display: grid;
690            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
691            gap: 8px;
692            max-height: 200px;
693            overflow-y: auto;
694        }
695        
696        .wb-desc-checkbox-item {
697            display: flex;
698            align-items: center;
699            gap: 8px;
700        }
701        
702        .wb-desc-checkbox-item input[type="checkbox"] {
703            cursor: pointer;
704        }
705        
706        .wb-desc-checkbox-item label {
707           	cursor: pointer;
708            font-size: 15px;
709            color: #374151;
710            margin: 0;
711        }
712        
713        .wb-progress-wrapper {
714            margin: 16px 0;
715        }
716        
717        .wb-progress-label {
718            display: flex;
719            justify-content: space-between;
720            margin-bottom: 8px;
721            font-size: 14px;
722            color: #374151;
723        }
724        
725        .wb-progress-bar-container {
726            width: 100%;
727            height: 8px;
728            background: #e5e7eb;
729            border-radius: 4px;
730            overflow: hidden;
731        }
732        
733        .wb-progress-bar-fill {
734            height: 100%;
735            background: linear-gradient(90deg, #9333ea, #6366f1);
736            transition: width 0.3s ease;
737            border-radius: 4px;
738        }
739        
740        .wb-progress-details {
741            margin-top: 8px;
742            font-size: 13px;
743            color: #6b7280;
744        }
745        
746        .wb-desc-stats {
747            margin-top: 12px;
748            padding: 14px;
749            background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
750            border-radius: 8px;
751            font-size: 14px;
752        }
753        
754        .wb-desc-stats-row {
755            display: flex;
756            justify-content: space-between;
757            margin-bottom: 6px;
758        }
759        
760        .wb-desc-stats-row:last-child {
761            margin-bottom: 0;
762        }
763        
764        .wb-desc-usage-link {
765            color: #6366f1;
766            text-decoration: underline;
767            cursor: pointer;
768            font-size: 14px;
769        }
770        
771        .wb-desc-usage-link:hover {
772            color: #4f46e5;
773        }
774        
775        .wb-desc-analytics-modal {
776            position: fixed;
777            top: 0;
778            left: 0;
779            width: 100%;
780            height: 100%;
781            background: rgba(0, 0, 0, 0.5);
782            display: flex;
783            align-items: center;
784            justify-content: center;
785            z-index: 10001;
786        }
787        
788        .wb-desc-analytics-content {
789            background: white;
790            border-radius: 12px;
791            padding: 24px;
792            max-width: 900px;
793            width: 95%;
794            max-height: 85vh;
795            overflow-y: auto;
796            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
797            z-index: 10000;
798        }
799        
800        .wb-desc-query-item {
801            padding: 10px 14px;
802            margin-bottom: 6px;
803            border-radius: 8px;
804            font-size: 14px;
805            display: flex;
806            justify-content: space-between;
807            align-items: center;
808            transition: all 0.2s;
809        }
810        
811        .wb-desc-query-item.used {
812            background: #d1fae5;
813            color: #065f46;
814        }
815        
816        .wb-desc-query-item.unused {
817            background: #f3f4f6;
818            color: #6b7280;
819        }
820        
821        .wb-desc-query-item.recoverable {
822            background: #fef3c7;
823            border-left: 3px solid #f59e0b;
824        }
825        
826        .wb-desc-query-text {
827            flex: 1;
828        }
829        
830        .wb-desc-query-popularity {
831            font-weight: 600;
832            margin-left: 12px;
833            min-width: 50px;
834            text-align: right;
835        }
836        
837        .wb-desc-minus-words-section {
838            margin-bottom: 16px;
839            padding: 14px;
840            background: linear-gradient(135deg, #fef3c7, #fde68a);
841            border-radius: 8px;
842            border: 1px solid #fbbf24;
843        }
844        
845        .wb-desc-minus-words-header {
846            font-weight: 600;
847            margin-bottom: 10px;
848            color: #92400e;
849            font-size: 15px;
850        }
851        
852        .wb-desc-minus-words-list {
853            display: flex;
854            flex-wrap: wrap;
855            gap: 8px;
856        }
857        
858        .wb-desc-minus-word-chip {
859            background: #fbbf24;
860            color: #78350f;
861            padding: 6px 12px;
862            border-radius: 16px;
863            font-size: 14px;
864            display: flex;
865            align-items: center;
866            gap: 6px;
867            cursor: pointer;
868            transition: all 0.2s;
869        }
870        
871        .wb-desc-minus-word-chip:hover {
872            background: #f59e0b;
873            transform: translateY(-1px);
874        }
875        
876        .wb-desc-minus-word-remove {
877            font-weight: bold;
878            font-size: 16px;
879        }
880        
881        .wb-desc-query-word {
882            cursor: pointer;
883            padding: 2px 4px;
884            border-radius: 3px;
885            transition: background 0.2s;
886        }
887        
888        .wb-desc-query-word:hover {
889            background: #fef3c7;
890        }
891        
892        .wb-desc-search-input {
893            width: 100%;
894            padding: 12px 14px;
895            border: 1px solid #d1d5db;
896            border-radius: 8px;
897            font-size: 15px;
898            margin-bottom: 16px;
899            box-sizing: border-box;
900        }
901        
902        .wb-desc-search-input:focus {
903            outline: none;
904            border-color: #9333ea;
905            box-shadow: 0 0 0 3px rgba(147, 51, 234, 0.1);
906        }
907        
908        .wb-auto-btn {
909            position: fixed;
910            top: 20px;
911            right: 20px;
912            padding: 12px 24px;
913            background: linear-gradient(135deg, #10b981, #059669);
914            color: white;
915            border: none;
916            border-radius: 8px;
917            font-size: 16px;
918            font-weight: 600;
919            cursor: pointer;
920            transition: all 0.2s;
921            z-index: 9999;
922            box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
923        }
924        
925        .wb-auto-btn:hover {
926            background: linear-gradient(135deg, #059669, #047857);
927            transform: translateY(-2px);
928            box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
929        }
930        
931        .wb-auto-btn:disabled {
932            background: #9ca3af;
933            cursor: not-allowed;
934            transform: none;
935            box-shadow: none;
936        }
937        
938        .wb-progress-modal {
939            position: fixed;
940            top: 20px;
941            right: 20px;
942            background: white;
943            border-radius: 12px;
944            padding: 20px;
945            min-width: 350px;
946            max-width: 400px;
947            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
948            z-index: 10000;
949        }
950        
951        .wb-progress-header {
952            font-size: 18px;
953            font-weight: 600;
954            margin-bottom: 16px;
955            color: #001a34;
956        }
957        
958        .wb-progress-stats {
959            display: flex;
960            gap: 16px;
961            margin-bottom: 16px;
962        }
963        
964        .wb-progress-stat {
965            flex: 1;
966            padding: 12px;
967            border-radius: 8px;
968            text-align: center;
969        }
970        
971        .wb-progress-stat.success {
972            background: linear-gradient(135deg, #d1fae5, #a7f3d0);
973            color: #065f46;
974        }
975        
976        .wb-progress-stat.error {
977            background: linear-gradient(135deg, #fee2e2, #fecaca);
978            color: #991b1b;
979        }
980        
981        .wb-progress-stat-number {
982            font-size: 28px;
983            font-weight: 700;
984            margin-bottom: 4px;
985        }
986        
987        .wb-progress-stat-label {
988            font-size: 12px;
989            text-transform: uppercase;
990            letter-spacing: 0.5px;
991        }
992        
993        .wb-progress-current {
994            padding: 12px;
995            background: #f3f4f6;
996            border-radius: 8px;
997            margin-bottom: 16px;
998            font-size: 14px;
999            color: #374151;
1000        }
1001        
1002        .wb-progress-errors {
1003            max-height: 200px;
1004            overflow-y: auto;
1005            margin-bottom: 16px;
1006        }
1007        
1008        .wb-progress-error-item {
1009            padding: 10px 12px;
1010            background: #fee2e2;
1011            border-radius: 6px;
1012            margin-bottom: 8px;
1013            font-size: 13px;
1014            color: #991b1b;
1015            cursor: pointer;
1016            transition: background 0.2s;
1017        }
1018        
1019        .wb-progress-error-item:hover {
1020            background: #fecaca;
1021        }
1022        
1023        .wb-progress-query-word {
1024            cursor: pointer;
1025            padding: 2px 4px;
1026            border-radius: 3px;
1027            transition: background 0.2s;
1028        }
1029        
1030        .wb-progress-query-word:hover {
1031            background: #fef3c7;
1032        }
1033        
1034        .wb-progress-buttons {
1035            display: flex;
1036            gap: 12px;
1037        }
1038        
1039        .wb-progress-btn {
1040            flex: 1;
1041            padding: 10px;
1042            border: none;
1043            border-radius: 8px;
1044            font-size: 14px;
1045            font-weight: 500;
1046            cursor: pointer;
1047            transition: all 0.2s;
1048        }
1049        
1050        .wb-progress-btn-stop {
1051            background: linear-gradient(135deg, #ef4444, #dc2626);
1052            color: white;
1053        }
1054        
1055        .wb-progress-btn-stop:hover {
1056            background: linear-gradient(135deg, #dc2626, #b91c1c);
1057        }
1058        
1059        .wb-progress-btn-close {
1060            background: #e5e7eb;
1061            color: #374151;
1062        }
1063        
1064        .wb-progress-btn-close:hover {
1065            background: #d1d5db;
1066        }
1067    `);
1068
1069    // ============================================
1070    // ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
1071    // ============================================
1072
1073    let lastUrl = location.href;
1074    let autoGenerationStopped = false;
1075    
1076    // Добавляем глобальные команды для тестирования сразу
1077    if (typeof window.WB_TEST === 'undefined') {
1078        window.WB_TEST = {
1079            enableTestMode: async (startFromIndex = 19) => {
1080                await GM.setValue('wb_test_mode', 'true');
1081                await GM.setValue('wb_test_start_index', startFromIndex);
1082                console.log(`🧪 ТЕСТОВЫЙ РЕЖИМ ВКЛЮЧЕН: Начало с товара #${startFromIndex}`);
1083                console.log('Теперь нажмите кнопку "🤖 Авто описание"');
1084            },
1085            disableTestMode: async () => {
1086                await GM.setValue('wb_test_mode', 'false');
1087                await GM.setValue('wb_test_start_index', 0);
1088                console.log('🧪 ТЕСТОВЫЙ РЕЖИМ ОТКЛЮЧЕН');
1089            },
1090            checkStatus: async () => {
1091                const testMode = await GM.getValue('wb_test_mode', 'false');
1092                const startIndex = await GM.getValue('wb_test_start_index', 0);
1093                const processed = await GM.getValue('wb_auto_processed_products', '[]');
1094                console.log('📊 СТАТУС:');
1095                console.log('  Тестовый режим:', testMode);
1096                console.log('  Стартовый индекс:', startIndex);
1097                console.log('  Обработано товаров:', JSON.parse(processed).length);
1098            },
1099            reset: async () => {
1100                await GM.setValue('wb_auto_processed_products', JSON.stringify([]));
1101                await GM.setValue('wb_test_mode', 'false');
1102                await GM.setValue('wb_test_start_index', 0);
1103                console.log('🔄 ВСЕ НАСТРОЙКИ СБРОШЕНЫ');
1104            }
1105        };
1106        
1107        console.log('🧪 ТЕСТОВЫЕ КОМАНДЫ ДОСТУПНЫ:');
1108        console.log('  WB_TEST.enableTestMode(19) - включить тест с 19-го товара');
1109        console.log('  WB_TEST.disableTestMode() - выключить тестовый режим');
1110        console.log('  WB_TEST.checkStatus() - проверить статус');
1111        console.log('  WB_TEST.reset() - сбросить все настройки');
1112    }
1113
1114    // ============================================
1115    // МОНИТОРИНГ URL (SPA)
1116    // ============================================
1117
1118    const debouncedUrlCheck = debounce(() => {
1119        const url = location.href;
1120        if (url !== lastUrl) {
1121            lastUrl = url;
1122            console.log('Wildberries Description Generator: URL изменился');
1123        
1124            if (url.includes('seller.wildberries.ru/new-goods/card')) {
1125                setTimeout(init, 1000);
1126            }
1127        }
1128    }, 300);
1129
1130    new MutationObserver(debouncedUrlCheck).observe(document, { subtree: true, childList: true });
1131
1132    const originalPushState = history.pushState;
1133    history.pushState = function() {
1134        originalPushState.apply(this, arguments);
1135        setTimeout(() => {
1136            if (location.href.includes('seller.wildberries.ru/new-goods/card')) {
1137                console.log('Wildberries Description Generator: History API навигация');
1138                setTimeout(init, 500);
1139            }
1140        }, 100);
1141    };
1142
1143    // ============================================
1144    // ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ТОВАРЕ
1145    // ============================================
1146
1147    function getProductInfo() {
1148        console.log('Wildberries Description Generator: Получение информации о товаре');
1149    
1150        const titleElement = document.querySelector('input[data-testid="card-form-main-field-name"]');
1151        const title = titleElement ? titleElement.value : '';
1152    
1153        console.log('Wildberries Description Generator: Поле названия найдено:', !!titleElement);
1154        console.log('Wildberries Description Generator: Название:', title || 'ПУСТО');
1155    
1156        let composition = '';
1157    
1158        const compositionLabel = Array.from(document.querySelectorAll('label, div')).find(el => 
1159            el.textContent && el.textContent.trim().toLowerCase() === 'состав'
1160        );
1161    
1162        console.log('Wildberries Description Generator: Label "Состав" найден:', !!compositionLabel);
1163    
1164        if (compositionLabel) {
1165            const parent = compositionLabel.closest('div[class*="Characteristics"]') || compositionLabel.parentElement;
1166            const compositionTextarea = parent ? parent.querySelector('textarea') : null;
1167            console.log('Wildberries Description Generator: Textarea состава найден:', !!compositionTextarea);
1168            if (compositionTextarea) {
1169                composition = compositionTextarea.value;
1170            }
1171        }
1172    
1173        if (!composition) {
1174            console.log('Wildberries Description Generator: Ищем состав через все textarea');
1175            const textareas = document.querySelectorAll('textarea');
1176            console.log('Wildberries Description Generator: Всего textarea на странице:', textareas.length);
1177            for (const textarea of textareas) {
1178                const value = textarea.value.toLowerCase();
1179                if (value.includes('кислота') || value.includes('масло') || value.includes('экстракт') || 
1180                    value.includes('витамин') || value.includes('глицерин') || value.length > 100) {
1181                    composition = textarea.value;
1182                    console.log('Wildberries Description Generator: Состав найден через поиск по textarea');
1183                    break;
1184                }
1185            }
1186        }
1187    
1188        console.log('Wildberries Description Generator: Состав:', composition ? composition.substring(0, 100) + '...' : 'НЕ НАЙДЕН');
1189    
1190        // Получаем форму выпуска
1191        let releaseForm = '';
1192        const releaseFormContainer = document.querySelector('.New-multi-select-input__value-container__Dvk75Ln3va');
1193        if (releaseFormContainer) {
1194            const releaseFormSpans = releaseFormContainer.querySelectorAll('span');
1195            const releaseFormValues = Array.from(releaseFormSpans).map(span => span.textContent.trim()).filter(text => text);
1196            releaseForm = releaseFormValues.join(', ');
1197            console.log('Wildberries Description Generator: Форма выпуска найдена:', releaseForm);
1198        } else {
1199            console.log('Wildberries Description Generator: Форма выпуска не найдена');
1200        }
1201    
1202        return { title, composition, releaseForm };
1203    }
1204
1205    // ============================================
1206    // СОЗДАНИЕ КНОПКИ ГЕНЕРАТОРА
1207    // ============================================
1208
1209    function createGeneratorButton() {
1210        const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
1211        
1212        if (!descriptionHeader) {
1213            console.log('Wildberries Description Generator: Заголовок описания не найден');
1214            return false;
1215        }
1216        
1217        if (document.querySelector('.wb-desc-generator-btn')) {
1218            console.log('Wildberries Description Generator: Кнопка уже добавлена');
1219            return true;
1220        }
1221        
1222        const button = document.createElement('button');
1223        button.className = 'wb-desc-generator-btn';
1224        button.textContent = '✨ Генератор описаний';
1225        button.type = 'button';
1226        button.addEventListener('click', function() {
1227            const expandButton = document.querySelector('div.Characteristics__expand__570w3PkC7D button');
1228            if (expandButton) {
1229                console.log('Wildberries Description Generator: Нажимаем "Показать все"');
1230                expandButton.click();
1231            } else {
1232                console.log('Wildberries Description Generator: Кнопка "Показать все" не найдена');
1233            }
1234            setTimeout(openModal, 500);
1235        });
1236        
1237        descriptionHeader.appendChild(button);
1238        console.log('Wildberries Description Generator: Кнопка добавлена');
1239        return true;
1240    }
1241
1242    // ============================================
1243    // ПОДСВЕТКА АНГЛИЙСКИХ СЛОВ
1244    // ============================================
1245
1246    function highlightEnglishWords(text) {
1247        const words = text.split(/(\s+)/);
1248        return words.map(word => {
1249            if (/[a-zA-Z]/.test(word)) {
1250                return `<span class="wb-desc-english-word">${word}</span>`;
1251            }
1252            return word;
1253        }).join('');
1254    }
1255
1256    // ============================================
1257    // ПРОГРЕСС-БАР
1258    // ============================================
1259
1260    function createProgressBar(container) {
1261        container.innerHTML = `
1262            <div class="wb-progress-wrapper">
1263                <div class="wb-progress-label">
1264                    <span id="wb-progress-stage">Подготовка...</span>
1265                    <span id="wb-progress-percent">0%</span>
1266                </div>
1267                <div class="wb-progress-bar-container">
1268                    <div class="wb-progress-bar-fill" id="wb-progress-fill" style="width: 0%"></div>
1269                </div>
1270                <div class="wb-progress-details" id="wb-progress-details"></div>
1271            </div>
1272        `;
1273    }
1274
1275    function updateProgressBar(stage, percent, details = '') {
1276        const stageEl = document.getElementById('wb-progress-stage');
1277        const percentEl = document.getElementById('wb-progress-percent');
1278        const fillEl = document.getElementById('wb-progress-fill');
1279        const detailsEl = document.getElementById('wb-progress-details');
1280        
1281        if (stageEl) stageEl.textContent = stage;
1282        if (percentEl) percentEl.textContent = `${percent}%`;
1283        if (fillEl) fillEl.style.width = `${percent}%`;
1284        if (detailsEl) detailsEl.textContent = details;
1285    }
1286
1287    // ============================================
1288    // МОДАЛЬНОЕ ОКНО
1289    // ============================================
1290
1291    async function openModal() {
1292        console.log('Wildberries Description Generator: Открытие модального окна');
1293        
1294        const urlParams = new URLSearchParams(window.location.search);
1295        const currentNmID = urlParams.get('nmID');
1296        console.log('Wildberries Description Generator: Текущий nmID:', currentNmID);
1297        
1298        let savedKeywords = '';
1299        let savedMinusWords = '';
1300        let savedCustomPrompt = '';
1301        
1302        if (currentNmID) {
1303            savedKeywords = await GM.getValue(`wb_product_${currentNmID}_keywords`, '');
1304            savedMinusWords = await GM.getValue(`wb_product_${currentNmID}_minus_words`, '');
1305            savedCustomPrompt = await GM.getValue(`wb_product_${currentNmID}_custom_prompt`, '');
1306            console.log('Wildberries Description Generator: Загружены сохраненные данные для товара', currentNmID);
1307        }
1308        
1309        const savedPrompts = await GM.getValue('wb_saved_prompts', '[]');
1310        const promptsList = JSON.parse(savedPrompts);
1311        
1312        const modal = document.createElement('div');
1313        modal.className = 'wb-desc-modal';
1314        modal.innerHTML = `
1315            <div class="wb-desc-modal-content">
1316                <div class="wb-desc-modal-header">✨ Генератор описаний для Wildberries</div>
1317                
1318                <div class="wb-desc-input-group">
1319                    <label class="wb-desc-label">Введите ключевые слова (каждое с новой строки):</label>
1320                    <textarea class="wb-desc-textarea" id="wb-keywords-input" placeholder="Например:&#10;сыворотка для лица&#10;витамин с&#10;увлажнение">${savedKeywords}</textarea>
1321                    <button class="wb-desc-suggest-btn" id="wb-suggest-keywords-btn">🔍 Предложить поисковые маски</button>
1322                    <div id="wb-suggested-keywords-container" style="display: none;"></div>
1323                </div>
1324                
1325                <div class="wb-desc-input-group">
1326                    <label class="wb-desc-label">Минус-слова (каждое с новой строки):</label>
1327                    <textarea class="wb-desc-textarea" style="min-height: 80px;" id="wb-minus-words-input" placeholder="Например:&#10;mixit&#10;nivea&#10;корея">${savedMinusWords}</textarea>
1328                </div>
1329                
1330                <div class="wb-desc-input-group">
1331                    <label class="wb-desc-label">
1332                        Дополнительные указания для AI (необязательно):
1333                        <span style="font-size: 13px; color: #6b7280; font-weight: normal; margin-left: 8px;">
1334                            Например: "Это товар для спортсменов", "Акцент на натуральность"
1335                        </span>
1336                    </label>
1337                    <textarea class="wb-desc-textarea" style="min-height: 80px;" id="wb-custom-prompt-input" placeholder="Укажите категорию товара, целевую аудиторию, особенности или акценты...">${savedCustomPrompt}</textarea>
1338                    <div style="display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap;">
1339                        <button class="wb-desc-suggest-btn" id="wb-save-prompt-btn" style="padding: 6px 14px; font-size: 14px;">
1340                            💾 Сохранить промпт
1341                        </button>
1342                        <button class="wb-desc-suggest-btn" id="wb-load-prompt-btn" style="padding: 6px 14px; font-size: 14px;">
1343                            📂 Загрузить промпт
1344                        </button>
1345                        ${promptsList.length > 0 ? `
1346                            <button class="wb-desc-suggest-btn" id="wb-manage-prompts-btn" style="padding: 6px 14px; font-size: 14px;">
1347                                ⚙️ Управление (${promptsList.length})
1348                            </button>
1349                        ` : ''}
1350                    </div>
1351                </div>
1352                
1353                <div id="wb-desc-result-container" style="display: none;">
1354                    <div class="wb-desc-label">Сгенерированное описание (можно редактировать):</div>
1355                    <div class="wb-desc-result-editable" id="wb-desc-result" contenteditable="true"></div>
1356                    <div class="wb-desc-char-count" id="wb-char-count"></div>
1357                    <div id="wb-desc-stats-container"></div>
1358                </div>
1359                
1360                <div id="wb-desc-status-container"></div>
1361                
1362                <div class="wb-desc-buttons">
1363                    <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-btn">Закрыть</button>
1364                    <button class="wb-desc-btn wb-desc-btn-primary" id="wb-generate-btn">🚀 Сгенерировать</button>
1365                    <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-btn" style="display: none;">🔄 Перегенерировать</button>
1366                    <button class="wb-desc-btn wb-desc-btn-success" id="wb-insert-btn" style="display: none;">✅ Вставить в описание</button>
1367                </div>
1368            </div>
1369        `;
1370        
1371        document.body.appendChild(modal);
1372        
1373        modal.addEventListener('click', (e) => {
1374            if (e.target === modal) {
1375                modal.remove();
1376            }
1377        });
1378        
1379        document.getElementById('wb-close-btn').addEventListener('click', () => {
1380            modal.remove();
1381        });
1382        
1383        document.getElementById('wb-suggest-keywords-btn').addEventListener('click', () => {
1384            suggestKeywords();
1385        });
1386        
1387        document.getElementById('wb-save-prompt-btn').addEventListener('click', () => {
1388            saveCustomPrompt();
1389        });
1390        
1391        document.getElementById('wb-load-prompt-btn').addEventListener('click', () => {
1392            loadCustomPrompt();
1393        });
1394        
1395        const manageBtn = document.getElementById('wb-manage-prompts-btn');
1396        if (manageBtn) {
1397            manageBtn.addEventListener('click', () => {
1398                managePrompts();
1399            });
1400        }
1401        
1402        document.getElementById('wb-generate-btn').addEventListener('click', () => {
1403            generateDescription(modal);
1404        });
1405        
1406        document.getElementById('wb-regenerate-btn').addEventListener('click', () => {
1407            generateDescription(modal, true);
1408        });
1409        
1410        document.getElementById('wb-insert-btn').addEventListener('click', () => {
1411            insertDescription(modal);
1412        });
1413    }
1414
1415    // ============================================
1416    // ПРЕДЛОЖЕНИЕ КЛЮЧЕВЫХ СЛОВ / МАСОК
1417    // ============================================
1418
1419    async function suggestKeywords() {
1420        const keywordsInput = document.getElementById('wb-keywords-input');
1421        const suggestBtn = document.getElementById('wb-suggest-keywords-btn');
1422        const suggestedContainer = document.getElementById('wb-suggested-keywords-container');
1423        const statusContainer = document.getElementById('wb-desc-status-container');
1424        
1425        const keywordsText = keywordsInput.value.trim();
1426        
1427        if (!keywordsText) {
1428            showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
1429            return;
1430        }
1431        
1432        suggestBtn.disabled = true;
1433        suggestBtn.textContent = '⏳ AI анализирует...';
1434        showStatus(statusContainer, 'AI анализирует товар и подбирает поисковые маски и ключевые слова...', 'info');
1435        
1436        try {
1437            const productInfo = getProductInfo();
1438            
1439            const suggestPrompt = generateMasksPrompt(productInfo);
1440
1441            console.log('Wildberries Description Generator: Запрос масок от AI');
1442            
1443            const suggestResponse = await RM.aiCall(suggestPrompt);
1444            
1445            let masks = [];
1446            let aiKeywords = [];
1447            try {
1448                const suggestData = JSON.parse(suggestResponse);
1449                masks = Array.isArray(suggestData.masks) ? suggestData.masks.filter(Boolean) : [];
1450                aiKeywords = Array.isArray(suggestData.keywords) ? suggestData.keywords.filter(Boolean) : [];
1451                console.log(`Wildberries Description Generator: AI предложил ${masks.length} масок и ${aiKeywords.length} ключей`);
1452            } catch (e) {
1453                console.error('Wildberries Description Generator: Ошибка парсинга масок:', e);
1454                showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
1455                return;
1456            }
1457            
1458            if (masks.length === 0 && aiKeywords.length === 0) {
1459                showStatus(statusContainer, 'AI не смог предложить маски и ключевые слова', 'error');
1460                return;
1461            }
1462            
1463            const hasChips = masks.length + aiKeywords.length > 0;
1464            
1465            suggestedContainer.innerHTML = `
1466                <div class="wb-desc-masks-container">
1467                    <div class="wb-desc-masks-header">
1468                        <span>Поисковые маски и ключевые слова (кликните для выбора):</span>
1469                        ${hasChips ? `<button class="wb-desc-suggest-btn" id="wb-toggle-all-btn" style="padding: 4px 12px; font-size: 13px;">
1470                            Выбрать все
1471                        </button>` : ''}
1472                    </div>
1473                    ${masks.length ? `
1474                        <div class="wb-desc-masks-group">
1475                            <div class="wb-desc-masks-group-title">МАСКИ</div>
1476                            <div class="wb-desc-masks-grid">
1477                                ${masks.map(mask => `
1478                                    <div class="wb-mask-chip" data-type="маска" data-mask="${mask}">
1479                                        <span>${mask}</span>
1480                                    </div>
1481                                `).join('')}
1482                            </div>
1483                        </div>
1484                    ` : '<div style="padding: 8px 0; color: #6b7280; text-align: center;">AI не предложил маски</div>'}
1485                    ${aiKeywords.length ? `
1486                        <div class="wb-desc-masks-group">
1487                            <div class="wb-desc-masks-group-title">КЛЮЧЕВЫЕ СЛОВА</div>
1488                            <div class="wb-desc-masks-grid">
1489                                ${aiKeywords.map(keyword => `
1490                                    <div class="wb-mask-chip" data-type="ключ" data-mask="${keyword}">
1491                                        <span>${keyword}</span>
1492                                    </div>
1493                                `).join('')}
1494                            </div>
1495                        </div>
1496                    ` : ''}
1497                </div>
1498            `;
1499            
1500            suggestedContainer.style.display = 'block';
1501            
1502            suggestedContainer.querySelectorAll('.wb-mask-chip').forEach(chip => {
1503                chip.addEventListener('click', () => {
1504                    chip.classList.toggle('selected');
1505                    updateToggleButtonText();
1506                });
1507            });
1508            
1509            function updateToggleButtonText() {
1510                const chips = suggestedContainer.querySelectorAll('.wb-mask-chip');
1511                const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1512                const toggleBtn = document.getElementById('wb-toggle-all-btn');
1513                if (toggleBtn) {
1514                    toggleBtn.textContent = allSelected ? 'Снять все' : 'Выбрать все';
1515                }
1516            }
1517            
1518            const toggleBtn = document.getElementById('wb-toggle-all-btn');
1519            if (toggleBtn) {
1520                toggleBtn.addEventListener('click', () => {
1521                    const chips = suggestedContainer.querySelectorAll('.wb-mask-chip');
1522                    const allSelected = Array.from(chips).every(c => c.classList.contains('selected'));
1523                    
1524                    chips.forEach(c => {
1525                        if (allSelected) {
1526                            c.classList.remove('selected');
1527                        } else {
1528                            c.classList.add('selected');
1529                        }
1530                    });
1531                    
1532                    updateToggleButtonText();
1533                });
1534            }
1535            
1536            showStatus(statusContainer, `AI предложил ${masks.length} масок и ${aiKeywords.length} ключевых слов. Выберите нужные и нажмите "Сгенерировать"`, 'success');
1537            
1538        } catch (error) {
1539            console.error('Wildberries Description Generator: Ошибка при предложении масок:', error);
1540            showStatus(statusContainer, 'Ошибка при получении предложений: ' + error.message, 'error');
1541        } finally {
1542            suggestBtn.disabled = false;
1543            suggestBtn.textContent = '🔍 Предложить поисковые маски';
1544        }
1545    }
1546
1547    // ============================================
1548    // СОХРАНЕНИЕ И ЗАГРУЗКА ПРОМПТОВ
1549    // ============================================
1550
1551    async function saveCustomPrompt() {
1552        const customPromptInput = document.getElementById('wb-custom-prompt-input');
1553        const promptText = customPromptInput.value.trim();
1554        
1555        if (!promptText) {
1556            alert('Введите текст промпта для сохранения');
1557            return;
1558        }
1559        
1560        const promptName = prompt('Введите название для этого промпта:');
1561        if (!promptName) return;
1562        
1563        const savedPrompts = await GM.getValue('wb_saved_prompts', '[]');
1564        const promptsList = JSON.parse(savedPrompts);
1565        
1566        promptsList.push({
1567            name: promptName,
1568            text: promptText,
1569            date: new Date().toISOString()
1570        });
1571        
1572        await GM.setValue('wb_saved_prompts', JSON.stringify(promptsList));
1573        alert('✅ Промпт сохранен!');
1574        
1575        location.reload();
1576    }
1577
1578    async function loadCustomPrompt() {
1579        const savedPrompts = await GM.getValue('wb_saved_prompts', '[]');
1580        const promptsList = JSON.parse(savedPrompts);
1581        
1582        if (promptsList.length === 0) {
1583            alert('У вас нет сохраненных промптов');
1584            return;
1585        }
1586        
1587        const promptNames = promptsList.map((p, i) => `${i + 1}. ${p.name}`).join('\n');
1588        const selection = prompt(`Выберите промпт (введите номер):\n\n${promptNames}`);
1589        
1590        if (!selection) return;
1591        
1592        const index = parseInt(selection) - 1;
1593        if (index >= 0 && index < promptsList.length) {
1594            const customPromptInput = document.getElementById('wb-custom-prompt-input');
1595            customPromptInput.value = promptsList[index].text;
1596            alert('✅ Промпт загружен!');
1597        } else {
1598            alert('Неверный номер');
1599        }
1600    }
1601
1602    async function managePrompts() {
1603        const savedPrompts = await GM.getValue('wb_saved_prompts', '[]');
1604        const promptsList = JSON.parse(savedPrompts);
1605        
1606        if (promptsList.length === 0) {
1607            alert('У вас нет сохраненных промптов');
1608            return;
1609        }
1610        
1611        const promptNames = promptsList.map((prompt, index) => `
1612            <div style="padding: 12px; margin-bottom: 8px; background: #f3f4f6; border-radius: 8px;">
1613                <div style="font-weight: 600; margin-bottom: 4px;">${prompt.name}</div>
1614                <div style="font-size: 13px; color: #6b7280; margin-bottom: 8px;">
1615                    ${new Date(prompt.date).toLocaleDateString('ru-RU')}
1616                </div>
1617                <div style="font-size: 14px; margin-bottom: 8px; max-height: 60px; overflow: hidden;">
1618                    ${prompt.text.substring(0, 100)}${prompt.text.length > 100 ? '...' : ''}
1619                </div>
1620                <button class="wb-desc-btn wb-desc-btn-secondary" data-action="delete" data-index="${index}" style="padding: 6px 12px; font-size: 14px;">
1621                    🗑️ Удалить
1622                </button>
1623            </div>
1624        `).join('');
1625        
1626        const modal = document.createElement('div');
1627        modal.className = 'wb-desc-modal';
1628        modal.innerHTML = `
1629            <div class="wb-desc-modal-content">
1630                <div class="wb-desc-modal-header">⚙️ Управление промптами</div>
1631                
1632                <div style="max-height: 400px; overflow-y: auto;">
1633                    ${promptNames}
1634                </div>
1635                
1636                <div class="wb-desc-buttons">
1637                    <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-manage-btn">Закрыть</button>
1638                </div>
1639            </div>
1640        `;
1641        
1642        document.body.appendChild(modal);
1643        
1644        modal.addEventListener('click', (e) => {
1645            if (e.target === modal) {
1646                modal.remove();
1647            }
1648        });
1649        
1650        document.getElementById('wb-close-manage-btn').addEventListener('click', () => {
1651            modal.remove();
1652        });
1653        
1654        modal.querySelectorAll('[data-action="delete"]').forEach(btn => {
1655            btn.addEventListener('click', async () => {
1656                const index = parseInt(btn.dataset.index);
1657                if (confirm('Удалить этот промпт?')) {
1658                    promptsList.splice(index, 1);
1659                    await GM.setValue('wb_saved_prompts', JSON.stringify(promptsList));
1660                    modal.remove();
1661                    alert('✅ Промпт удален!');
1662                    location.reload();
1663                }
1664            });
1665        });
1666    }
1667
1668    // ============================================
1669    // СБОР ДАННЫХ С АНАЛИТИКИ
1670    // ============================================
1671
1672    async function collectAnalyticsData(keywords, minusWords) {
1673        console.log('Wildberries Description Generator: Начало сбора данных с аналитики');
1674        
1675        await GM.setValue('wb_keywords_to_process', JSON.stringify(keywords));
1676        await GM.setValue('wb_minus_words', JSON.stringify(minusWords));
1677        await GM.setValue('wb_analytics_data', JSON.stringify([]));
1678        await GM.setValue('wb_collection_status', 'pending');
1679        
1680        const analyticsUrl = 'https://seller.wildberries.ru/search-analytics/popular-search-queries';
1681        await GM.openInTab(analyticsUrl, false);
1682        
1683        console.log('Wildberries Description Generator: Открыта страница аналитики, ожидание сбора данных...');
1684        
1685        const maxWaitTime = 300000;
1686        const checkInterval = 2000;
1687        let waitedTime = 0;
1688        
1689        while (waitedTime < maxWaitTime) {
1690            await new Promise(resolve => setTimeout(resolve, checkInterval));
1691            waitedTime += checkInterval;
1692            
1693            const status = await GM.getValue('wb_collection_status', 'pending');
1694            
1695            if (status === 'completed') {
1696                const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
1697                const analyticsData = JSON.parse(analyticsDataStr);
1698                console.log('Wildberries Description Generator: Данные успешно собраны');
1699                return analyticsData;
1700            } else if (status === 'error') {
1701                console.error('Wildberries Description Generator: Ошибка при сборе данных');
1702                return [];
1703            }
1704            
1705            const progress = Math.min(60, 25 + Math.floor((waitedTime / maxWaitTime) * 35));
1706            updateProgressBar('Сбор данных из аналитики...', progress, `Прошло ${Math.floor(waitedTime / 1000)} сек.`);
1707        }
1708        
1709        console.error('Wildberries Description Generator: Превышено время ожидания сбора данных');
1710        return [];
1711    }
1712
1713    // ============================================
1714    // АВТОМАТИЧЕСКИЙ СБОР НА СТРАНИЦЕ АНАЛИТИКИ
1715    // ============================================
1716
1717    async function autoCollectOnAnalyticsPage() {
1718        if (!window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
1719            return;
1720        }
1721        
1722        console.log('Wildberries Description Generator: Обнаружена страница аналитики');
1723        
1724        const status = await GM.getValue('wb_collection_status', 'none');
1725        if (status !== 'pending') {
1726            return;
1727        }
1728        
1729        console.log('Wildberries Description Generator: Начинаем автоматический сбор данных');
1730        
1731        try {
1732            const keywordsStr = await GM.getValue('wb_keywords_to_process', '[]');
1733            const minusWordsStr = await GM.getValue('wb_minus_words', '[]');
1734            const productAnalysisStr = await GM.getValue('wb_product_analysis', '{}');
1735            const keywords = JSON.parse(keywordsStr);
1736            const minusWords = JSON.parse(minusWordsStr);
1737            const productAnalysis = JSON.parse(productAnalysisStr);
1738            
1739            console.log('Wildberries Description Generator: AI-критерии фильтрации:', productAnalysis);
1740            
1741            const analyticsData = [];
1742            
1743            await new Promise(resolve => setTimeout(resolve, 3000));
1744            
1745            try {
1746                const monthButton = Array.from(document.querySelectorAll('button[data-name="Segments"]')).find(btn => 
1747                    btn.textContent && btn.textContent.trim().toLowerCase().includes('месяц')
1748                );
1749                
1750                if (monthButton) {
1751                    console.log('Wildberries Description Generator: Кнопка "Месяц" найдена, нажимаем');
1752                    monthButton.click();
1753                    await new Promise(resolve => setTimeout(resolve, 2000));
1754                    console.log('Wildberries Description Generator: Период "Месяц" выбран');
1755                } else {
1756                    console.log('Wildberries Description Generator: Кнопка "Месяц" не найдена, используем текущий период');
1757                }
1758            } catch (e) {
1759                console.error('Wildberries Description Generator: Ошибка при выборе периода:', e);
1760            }
1761            
1762            const compositionLower = (productAnalysis.composition || '').toLowerCase();
1763            const keyComponentsLower = (productAnalysis.key_components || []).map(c => c.toLowerCase());
1764            
1765            for (const keyword of keywords) {
1766                console.log(`Wildberries Description Generator: Обработка ключевого слова: ${keyword}`);
1767                
1768                try {
1769                    const searchInput = document.querySelector('input[name="searchString"]');
1770                    if (!searchInput) {
1771                        console.error('Wildberries Description Generator: Поле поиска не найдено');
1772                        continue;
1773                    }
1774                    
1775                    searchInput.value = '';
1776                    searchInput.focus();
1777                    searchInput.value = keyword;
1778                    searchInput.dispatchEvent(new Event('input', { bubbles: true }));
1779                    searchInput.dispatchEvent(new Event('change', { bubbles: true }));
1780                    
1781                    await new Promise(resolve => setTimeout(resolve, 5000));
1782                    
1783                    const rows = document.querySelectorAll('table tbody tr');
1784                    const keywordData = {
1785                        keyword: keyword,
1786                        queries: []
1787                    };
1788                    
1789                    console.log(`Wildberries Description Generator: Найдено строк в таблице: ${rows.length}`);
1790                    
1791                    rows.forEach(row => {
1792                        const cells = row.querySelectorAll('td');
1793                        if (cells.length >= 2) {
1794                            const query = cells[0]?.textContent?.trim();
1795                            const popularityText = cells[1]?.textContent?.trim();
1796                            
1797                            if (query && popularityText) {
1798                                const popularity = parseInt(popularityText.replace(/\s+/g, ''));
1799                                const queryLower = query.toLowerCase();
1800                                
1801                                const hasMinusWord = minusWords.some(minusWord => 
1802                                    queryLower.includes(minusWord.toLowerCase())
1803                                );
1804                                
1805                                if (hasMinusWord) {
1806                                    console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
1807                                    return;
1808                                }
1809                                
1810                                const autoBanList = [
1811                                    'mixit', 'axis', 'nivea', 'garnier', 'loreal', 'maybelline', 'vichy', 'bioderma',
1812                                    'эвалар', 'солгар', 'now foods', 'доппельгерц', 'артнео', 'гельтек',
1813                                    'корея', 'корейск', 'япония', 'японск', 'франция', 'французск', 'америк', 'китай', 'китайск',
1814                                    'купить', 'цена', 'отзыв', 'инструкция', 'доставка'
1815                                ];
1816                                
1817                                const hasAutoBan = autoBanList.some(banned => 
1818                                    queryLower.includes(banned)
1819                                );
1820                                
1821                                if (hasAutoBan) {
1822                                    console.log(`Wildberries Description Generator: Исключен запрос "${query}" (автофильтр: бренд/страна)`);
1823                                    return;
1824                                }
1825                                
1826                                const hasEnglish = /[a-z]/i.test(query);
1827                                
1828                                if (hasEnglish) {
1829                                    const allowedWords = productAnalysis.allowed_english_words || [];
1830                                    const isAllowedEnglish = allowedWords.some(allowed => 
1831                                        queryLower.includes(allowed.toLowerCase())
1832                                    );
1833                                    
1834                                    if (!isAllowedEnglish) {
1835                                        const normalizeText = (text) => {
1836                                            const latinToCyrillic = {
1837                                                'a': 'а', 'A': 'А', 'e': 'е', 'E': 'Е', 'o': 'о', 'O': 'О',
1838                                                'p': 'р', 'P': 'Р', 'c': 'с', 'C': 'С', 'y': 'у', 'Y': 'У',
1839                                                'x': 'х', 'X': 'Х', 'k': 'к', 'K': 'К', 'h': 'н', 'H': 'Н',
1840                                                'b': 'в', 'B': 'В', 'm': 'м', 'M': 'М', 't': 'т', 'T': 'Т'
1841                                            };
1842                                            return text.split('').map(char => latinToCyrillic[char] || char).join('').toLowerCase();
1843                                        };
1844                                        
1845                                        const normalizedQuery = normalizeText(query);
1846                                        const isSimilarToUserKeyword = keywords.some(k => {
1847                                            const normalizedKeyword = normalizeText(k);
1848                                            return normalizedQuery === normalizedKeyword;
1849                                        });
1850                                        
1851                                        if (!isSimilarToUserKeyword) {
1852                                            console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит неразрешенные английские слова)`);
1853                                            return;
1854                                        }
1855                                    } else {
1856                                        console.log(`Wildberries Description Generator: Разрешен запрос "${query}" (содержит разрешенное английское слово)`);
1857                                    }
1858                                }
1859                                
1860                                const excludedPurposes = productAnalysis.excluded_purposes || [];
1861                                const hasExcludedPurpose = excludedPurposes.some(excluded => 
1862                                    queryLower.includes(excluded.toLowerCase())
1863                                );
1864                                
1865                                if (hasExcludedPurpose) {
1866                                    console.log(`Wildberries Description Generator: Исключен запрос "${query}" (неподходящее назначение по AI-критериям)`);
1867                                    return;
1868                                }
1869                                
1870                                const queryWords = queryLower.split(/\s+/);
1871                                const compositionWords = compositionLower.split(/\s+/);
1872                                
1873                                // УПРОЩЕННАЯ ПРОВЕРКА: только явные конфликты компонентов
1874                                // Список компонентов, которые ТОЧНО должны быть в составе, если упомянуты
1875                                const strictComponents = [
1876                                    'лютеин', 'зеаксантин', 'коллаген', 'кератин', 'ретинол', 
1877                                    'гиалуроновая', 'гиалуронов', 'коэнзим', 'биотин', 'омега',
1878                                    'цинк', 'селен', 'магний', 'кальций', 'железо'
1879                                ];
1880                                
1881                                // Проверяем только явные упоминания строгих компонентов
1882                                const hasStrictComponent = strictComponents.some(comp => {
1883                                    const mentioned = queryLower.includes(comp);
1884                                    if (!mentioned) return false;
1885                                    
1886                                    // Проверяем, есть ли этот компонент в составе
1887                                    const inComposition = compositionLower.includes(comp);
1888                                    const inKeyComponents = keyComponentsLower.some(kc => kc.includes(comp) || comp.includes(kc));
1889                                    
1890                                    if (!inComposition && !inKeyComponents) {
1891                                        console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит компонент "${comp}", которого нет в составе)`);
1892                                        return true; // Исключаем запрос
1893                                    }
1894                                    return false;
1895                                });
1896                                
1897                                if (hasStrictComponent) {
1898                                    return;
1899                                }
1900                                
1901                                keywordData.queries.push({
1902                                    query,
1903                                    popularity
1904                                });
1905                            }
1906                        }
1907                    });
1908                    
1909                    analyticsData.push(keywordData);
1910                    console.log(`Wildberries Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
1911                    
1912                } catch (error) {
1913                    console.error(`Wildberries Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
1914                }
1915            }
1916            
1917            await GM.setValue('wb_analytics_data', JSON.stringify(analyticsData));
1918            await GM.setValue('wb_collection_status', 'completed');
1919            
1920            console.log('Wildberries Description Generator: Сбор данных завершен, можно закрыть вкладку');
1921            
1922            setTimeout(() => {
1923                window.close();
1924            }, 2000);
1925            
1926        } catch (error) {
1927            console.error('Wildberries Description Generator: Ошибка при автоматическом сборе данных:', error);
1928            await GM.setValue('wb_collection_status', 'error');
1929        }
1930    }
1931
1932    // ============================================
1933    // ГЕНЕРАЦИЯ ОПИСАНИЯ
1934    // ============================================
1935
1936    async function generateDescription(modal, skipDataCollection = false) {
1937        console.log('Wildberries Description Generator: Генерация описания');
1938    
1939        const keywordsInput = document.getElementById('wb-keywords-input');
1940        const minusWordsInput = document.getElementById('wb-minus-words-input');
1941        const customPromptInput = document.getElementById('wb-custom-prompt-input');
1942        const generateBtn = document.getElementById('wb-generate-btn');
1943        const regenerateBtn = document.getElementById('wb-regenerate-btn');
1944        const insertBtn = document.getElementById('wb-insert-btn');
1945        const resultContainer = document.getElementById('wb-desc-result-container');
1946        const resultDiv = document.getElementById('wb-desc-result');
1947        const charCountDiv = document.getElementById('wb-char-count');
1948        const statusContainer = document.getElementById('wb-desc-status-container');
1949        const statsContainer = document.getElementById('wb-desc-stats-container');
1950    
1951        let keywordsText = keywordsInput.value.trim();
1952        const customPrompt = customPromptInput ? customPromptInput.value.trim() : '';
1953    
1954        const selectedSuggestions = Array.from(document.querySelectorAll('.wb-mask-chip.selected'))
1955            .map(chip => chip.dataset.mask);
1956    
1957        if (selectedSuggestions.length > 0) {
1958            const existingKeywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1959            const allKeywords = [...new Set([...existingKeywords, ...selectedSuggestions])];
1960            keywordsText = allKeywords.join('\n');
1961            console.log('Wildberries Description Generator: Добавлены маски/ключи:', selectedSuggestions);
1962        }
1963    
1964        const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1965        const minusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
1966    
1967        if (keywords.length === 0) {
1968            showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
1969            return;
1970        }
1971    
1972        const urlParams = new URLSearchParams(window.location.search);
1973        const currentNmID = urlParams.get('nmID');
1974        if (currentNmID) {
1975            await GM.setValue(`wb_product_${currentNmID}_keywords`, keywords.join('\n'));
1976            await GM.setValue(`wb_product_${currentNmID}_minus_words`, minusWords.join('\n'));
1977            await GM.setValue(`wb_product_${currentNmID}_custom_prompt`, customPrompt);
1978            console.log('Wildberries Description Generator: Сохранены ключевые слова, минус-слова и промпт для товара', currentNmID);
1979        }
1980    
1981        generateBtn.disabled = true;
1982        regenerateBtn.disabled = true;
1983    
1984        try {
1985            const productInfo = getProductInfo();
1986            let analyticsData = [];
1987    
1988            let queryPopularity = {};
1989    
1990            if (!skipDataCollection) {
1991                createProgressBar(statusContainer);
1992                updateProgressBar('AI анализирует товар...', 10);
1993    
1994                console.log('Wildberries Description Generator: AI анализирует товар для фильтрации');
1995                const analysisPrompt = generateProductAnalysisPrompt(productInfo, keywords);
1996    
1997                const analysisResponse = await RM.aiCall(analysisPrompt);
1998                const productAnalysis = JSON.parse(analysisResponse);
1999                console.log('Wildberries Description Generator: AI-анализ товара:', productAnalysis);
2000    
2001                await GM.setValue('wb_product_analysis', JSON.stringify(productAnalysis));
2002                await GM.setValue('wb_analytics_minus_words', JSON.stringify([]));
2003    
2004                updateProgressBar('Сбор данных из аналитики...', 20, 'Откроется новая вкладка');
2005    
2006                analyticsData = await collectAnalyticsData(keywords, minusWords);
2007    
2008                if (analyticsData.length === 0) {
2009                    showStatus(statusContainer, 'Не удалось собрать данные из аналитики', 'error');
2010                    return;
2011                }
2012            } else {
2013                const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
2014                analyticsData = JSON.parse(analyticsDataStr);
2015    
2016                if (analyticsData.length === 0) {
2017                    showStatus(statusContainer, 'Нет сохраненных данных. Пожалуйста, сначала соберите данные.', 'error');
2018                    return;
2019                }
2020            }
2021    
2022            const allQueries = [];
2023            analyticsData.forEach(data => {
2024                data.queries.forEach(q => {
2025                    allQueries.push(q.query);
2026                    queryPopularity[q.query.toLowerCase()] = q.popularity;
2027                });
2028            });
2029    
2030            console.log(`Wildberries Description Generator: Всего запросов для генерации: ${allQueries.length}`);
2031    
2032            if (allQueries.length === 0) {
2033                showStatus(statusContainer, 'Не найдено подходящих запросов для генерации', 'error');
2034                return;
2035            }
2036    
2037            console.log('Wildberries Description Generator: Применяем минус-слова перед генерацией');
2038            console.log('Wildberries Description Generator: Минус-слова:', minusWords);
2039            console.log('Wildberries Description Generator: Запросов до фильтрации:', allQueries.length);
2040    
2041            const filteredQueries = allQueries.filter(query => {
2042                const queryLower = query.toLowerCase();
2043                const hasMinusWord = minusWords.some(minusWord => 
2044                    queryLower.includes(minusWord.toLowerCase())
2045                );
2046    
2047                if (hasMinusWord) {
2048                    console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
2049                }
2050    
2051                return !hasMinusWord;
2052            });
2053    
2054            console.log('Wildberries Description Generator: Запросов после фильтрации:', filteredQueries.length);
2055    
2056            if (filteredQueries.length === 0) {
2057                showStatus(statusContainer, 'Все запросы отфильтрованы минус-словами. Попробуйте уменьшить количество минус-слов.', 'error');
2058                return;
2059            }
2060    
2061            if (!skipDataCollection) {
2062                updateProgressBar('AI генерирует описание...', 70);
2063            } else {
2064                showStatus(statusContainer, 'AI генерирует описание...', 'info');
2065            }
2066    
2067            const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity, customPrompt);
2068    
2069            const description = await RM.aiCall(descriptionPrompt);
2070
2071            await GM.setValue('wb_generated_description', description);
2072            await GM.setValue('wb_query_popularity', JSON.stringify(queryPopularity));
2073
2074            let cleanedDescription = description;
2075            const englishToRussian = {
2076                'crucial': 'ключевой',
2077                'Crucial': 'Ключевой',
2078                'essential': 'важный',
2079                'Essential': 'Важный',
2080                'vital': 'жизненно важный',
2081                'Vital': 'Жизненно важный',
2082                'key': 'ключевой',
2083                'Key': 'Ключевой',
2084                'important': 'важный',
2085                'Important': 'Важный',
2086                'testosterone': 'тестостерон',
2087                'Testosterone': 'Тестостерон',
2088                'energy': 'энергия',
2089                'Energy': 'Энергия'
2090            };
2091
2092            Object.entries(englishToRussian).forEach(([eng, rus]) => {
2093                const regex = new RegExp('\\b' + eng + '\\b', 'g');
2094                cleanedDescription = cleanedDescription.replace(regex, rus);
2095            });
2096
2097            resultDiv.innerHTML = highlightEnglishWords(cleanedDescription);
2098            resultContainer.style.display = 'block';
2099
2100            const charCount = cleanedDescription.length;
2101            charCountDiv.textContent = `Символов: ${charCount}`;
2102
2103            if (charCount >= 3500 && charCount <= 4000) {
2104                charCountDiv.className = 'wb-desc-char-count success';
2105            } else if (charCount < 3500) {
2106                charCountDiv.className = 'wb-desc-char-count warning';
2107            } else {
2108                charCountDiv.className = 'wb-desc-char-count error';
2109            }
2110
2111            resultDiv.addEventListener('input', function() {
2112                const currentText = resultDiv.innerText;
2113                const currentCount = currentText.length;
2114                charCountDiv.textContent = `Символов: ${currentCount}`;
2115
2116                if (currentCount >= 3500 && currentCount <= 4000) {
2117                    charCountDiv.className = 'wb-desc-char-count success';
2118                } else if (currentCount < 3500) {
2119                    charCountDiv.className = 'wb-desc-char-count warning';
2120                } else {
2121                    charCountDiv.className = 'wb-desc-char-count error';
2122                }
2123
2124                GM.setValue('wb_generated_description', currentText);
2125            });
2126
2127            if (statsContainer) {
2128                const newStatsContainer = document.createElement('div');
2129                newStatsContainer.id = 'wb-desc-stats-container';
2130                charCountDiv.parentElement.insertBefore(newStatsContainer, charCountDiv.nextSibling);
2131            }
2132
2133            const analysis = await analyzeUsedKeywords(cleanedDescription, queryPopularity);
2134            const usagePercent = Math.round(analysis.usedQueries.length / analysis.totalQueriesAvailable * 100);
2135    
2136            if (analysis.unusedQueries.length > 0) {
2137                console.log(`Wildberries Description Generator: Обнаружено ${analysis.unusedQueries.length} неиспользованных запросов`);
2138    
2139                const sortedUnused = analysis.unusedQueries
2140                    .map(query => ({
2141                        query: query,
2142                        popularity: analysis.queryPopularity[query.toLowerCase()] || 0
2143                    }))
2144                    .sort((a, b) => b.popularity - a.popularity);
2145    
2146                console.log('Wildberries Description Generator: Топ-10 пропущенных запросов:', 
2147                    sortedUnused.slice(0, 10).map(q => `${q.query} (${q.popularity})`));
2148    
2149                try {
2150                    const topUnused = sortedUnused.slice(0, 30).map(q => q.query);
2151    
2152                    const recoverablePrompt = `Проанализируй, почему эти запросы НЕ были использованы в описании товара.
2153    
2154ДАННЫЕ О ТОВАРЕ:
2155• Название: ${productInfo.title || 'не указано'}
2156• Состав: ${productInfo.composition || 'не указан'}
2157    
2158ОПИСАНИЕ (первые 1000 символов):
2159${cleanedDescription.substring(0, 1000)}...
2160    
2161НЕИСПОЛЬЗОВАННЫЕ ЗАПРОСЫ (топ-30 по популярности):
2162${topUnused.map((q, i) => `${i+1}. "${q}"`).join('\n')}
2163    
2164ЗАДАЧА:
2165Определи, какие из этих запросов МОЖНО было использовать, но AI пропустил.
2166    
2167КРИТЕРИИ для включения запроса:
21681. Запрос релевантен товару (описывает товар, его назначение или компоненты)
21692. Компонент из запроса есть в составе ИЛИ можно использовать через логику замены
21703. Запрос не противоречит назначению товара
21714. Запрос не содержит бренды конкурентов
2172    
2173Верни список запросов, которые МОЖНО было использовать, в формате JSON:
2174{
2175  "recoverable_queries": ["запрос 1", "запрос 2", ...],
2176  "reasons": {
2177    "запрос 1": "краткая причина почему можно использовать",
2178    "запрос 2": "краткая причина почему можно использовать"
2179  }
2180}
2181    
2182НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
2183    
2184                    const aiAnalysisResponse = await RM.aiCall(recoverablePrompt);
2185                    const aiAnalysis = JSON.parse(aiAnalysisResponse);
2186    
2187                    if (aiAnalysis.recoverable_queries && aiAnalysis.recoverable_queries.length > 0) {
2188                        console.log(`Wildberries Description Generator: AI нашел ${aiAnalysis.recoverable_queries.length} пропущенных запросов`);
2189                        console.log('Wildberries Description Generator: Причины:', aiAnalysis.reasons);
2190    
2191                        await GM.setValue('wb_recoverable_queries', JSON.stringify(aiAnalysis.recoverable_queries));
2192                        await GM.setValue('wb_recoverable_reasons', JSON.stringify(aiAnalysis.reasons));
2193                    } else {
2194                        console.log('Wildberries Description Generator: AI не нашел пропущенных запросов');
2195                        await GM.setValue('wb_recoverable_queries', JSON.stringify([]));
2196                        await GM.setValue('wb_recoverable_reasons', JSON.stringify({}));
2197                    }
2198                } catch (e) {
2199                    console.error('Wildberries Description Generator: Ошибка при анализе пропущенных запросов:', e);
2200                    await GM.setValue('wb_recoverable_queries', JSON.stringify([]));
2201                    await GM.setValue('wb_recoverable_reasons', JSON.stringify({}));
2202                }
2203            }
2204    
2205            if (!statsContainer) {
2206                const newStatsContainer = document.createElement('div');
2207                newStatsContainer.id = 'wb-desc-stats-container';
2208                charCountDiv.parentElement.insertBefore(newStatsContainer, charCountDiv.nextSibling);
2209            }
2210    
2211            document.getElementById('wb-desc-stats-container').innerHTML = `
2212                <div class="wb-desc-stats">
2213                    <div class="wb-desc-stats-row">
2214                        <span><strong>Использовано запросов:</strong></span>
2215                        <span>${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} (${usagePercent}%)</span>
2216                    </div>
2217                    <div class="wb-desc-stats-row">
2218                        <span><strong>Общая частотность:</strong></span>
2219                        <span>${formatNumber(analysis.totalPopularity)}</span>
2220                    </div>
2221                </div>
2222            `;
2223    
2224            generateBtn.style.display = 'none';
2225            regenerateBtn.style.display = 'inline-block';
2226            insertBtn.style.display = 'inline-block';
2227    
2228            showStatus(statusContainer, '✅ Описание успешно сгенерировано! <span class="wb-desc-usage-link" id="wb-show-analytics-link">Показать аналитику использования запросов</span>', 'success');
2229    
2230            setTimeout(() => {
2231                const analyticsLink = document.getElementById('wb-show-analytics-link');
2232                if (analyticsLink) {
2233                    analyticsLink.addEventListener('click', () => {
2234                        showUsageAnalytics();
2235                    });
2236                }
2237            }, 100);
2238    
2239        } catch (error) {
2240            console.error('Wildberries Description Generator: Ошибка при генерации описания:', error);
2241            showStatus(statusContainer, 'Ошибка при генерации: ' + error.message, 'error');
2242        } finally {
2243            generateBtn.disabled = false;
2244            regenerateBtn.disabled = false;
2245        }
2246    }
2247
2248    // ============================================
2249    // АНАЛИЗ ИСПОЛЬЗОВАННЫХ КЛЮЧЕВЫХ СЛОВ
2250    // ============================================
2251
2252    async function analyzeUsedKeywords(description, queryPopularityParam = null) {
2253        console.log('Wildberries Description Generator: Анализ использованных ключевых слов');
2254    
2255        const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
2256        const analyticsData = JSON.parse(analyticsDataStr);
2257    
2258        const allQueries = [];
2259        let queryPopularity = queryPopularityParam || {};
2260        
2261        if (!queryPopularityParam) {
2262            const savedPopularity = await GM.getValue('wb_query_popularity', '{}');
2263            queryPopularity = JSON.parse(savedPopularity);
2264        }
2265    
2266        analyticsData.forEach(data => {
2267            data.queries.forEach(q => {
2268                allQueries.push(q.query);
2269                if (!queryPopularity[q.query.toLowerCase()]) {
2270                    queryPopularity[q.query.toLowerCase()] = q.popularity;
2271                }
2272            });
2273        });
2274    
2275        const descriptionLower = description.toLowerCase();
2276        const usedQueries = [];
2277        const unusedQueries = [];
2278        let totalPopularity = 0;
2279    
2280        allQueries.forEach(query => {
2281            if (descriptionLower.includes(query.toLowerCase())) {
2282                usedQueries.push(query);
2283                totalPopularity += queryPopularity[query.toLowerCase()] || 0;
2284            } else {
2285                unusedQueries.push(query);
2286            }
2287        });
2288    
2289        console.log(`Wildberries Description Generator: Использовано ${usedQueries.length} из ${allQueries.length} запросов`);
2290    
2291        return {
2292            usedQueries,
2293            unusedQueries,
2294            totalQueriesAvailable: allQueries.length,
2295            totalPopularity,
2296            queryPopularity
2297        };
2298    }
2299
2300    // ============================================
2301    // ПОКАЗ АНАЛИТИКИ ИСПОЛЬЗОВАНИЯ ЗАПРОСОВ
2302    // ============================================
2303
2304    async function showUsageAnalytics() {
2305        console.log('Wildberries Description Generator: Показ аналитики использования');
2306    
2307        const description = await GM.getValue('wb_generated_description', '');
2308        if (!description) {
2309            alert('Описание не найдено');
2310            return;
2311        }
2312    
2313        const analysis = await analyzeUsedKeywords(description);
2314    
2315        const minusWordsStr = await GM.getValue('wb_analytics_minus_words', '[]');
2316        const minusWords = JSON.parse(minusWordsStr);
2317        
2318        const recoverableQueriesStr = await GM.getValue('wb_recoverable_queries', '[]');
2319        const recoverableQueries = JSON.parse(recoverableQueriesStr);
2320        const recoverableReasonsStr = await GM.getValue('wb_recoverable_reasons', '{}');
2321        const recoverableReasons = JSON.parse(recoverableReasonsStr);
2322        
2323        console.log(`Wildberries Description Generator: Загружено ${recoverableQueries.length} пропущенных запросов от AI`);
2324        
2325        let usedQueries = [...analysis.usedQueries];
2326        let unusedQueries = [...analysis.unusedQueries];
2327        let currentMinusWords = [...minusWords];
2328        let searchQuery = '';
2329    
2330        function renderModal() {
2331            const usedContainer = document.getElementById('wb-used-queries-container');
2332            const unusedContainer = document.getElementById('wb-unused-queries-container');
2333            const usedScrollTop = usedContainer ? usedContainer.scrollTop : 0;
2334            const unusedScrollTop = unusedContainer ? unusedContainer.scrollTop : 0;
2335            
2336            const filteredUsed = usedQueries.filter(q => 
2337                q.toLowerCase().includes(searchQuery.toLowerCase())
2338            );
2339            const filteredUnused = unusedQueries.filter(q => 
2340                q.toLowerCase().includes(searchQuery.toLowerCase())
2341            );
2342            
2343            const analyticsModal = document.querySelector('.wb-desc-analytics-modal');
2344            if (!analyticsModal) return;
2345            
2346            analyticsModal.innerHTML = `
2347            <div class="wb-desc-analytics-content">
2348                <div class="wb-desc-modal-header">📊 Аналитика использования запросов</div>
2349                
2350                ${currentMinusWords.length > 0 ? `
2351                <div class="wb-desc-minus-words-section">
2352                    <div class="wb-desc-minus-words-header">Минус-слова (клик для удаления):</div>
2353                    <div class="wb-desc-minus-words-list">
2354                        ${currentMinusWords.map(word => `
2355                            <div class="wb-desc-minus-word-chip" data-word="${word}">
2356                                ${word}
2357                                <span class="wb-desc-minus-word-remove">×</span>
2358                            </div>
2359                        `).join('')}
2360                    </div>
2361                </div>
2362                ` : ''}
2363                
2364                <input type="text" class="wb-desc-search-input" id="wb-analytics-search" placeholder="🔍 Поиск по запросам..." value="${searchQuery}">
2365                
2366                <div style="margin-bottom: 16px; display: flex; gap: 20px; flex-wrap: wrap;">
2367                    <div><strong>Использовано:</strong> ${usedQueries.length} из ${analysis.totalQueriesAvailable} (${Math.round(usedQueries.length / analysis.totalQueriesAvailable * 100)}%)</div>
2368                    <div><strong>Общая частотность:</strong> ${formatNumber(analysis.totalPopularity)}</div>
2369                    ${searchQuery ? `<div><strong>Найдено:</strong> ${filteredUsed.length + filteredUnused.length}</div>` : ''}
2370                </div>
2371                
2372                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
2373                    <div>
2374                        <div style="margin-bottom: 12px; font-weight: 600; color: #065f46;">✅ Использованные (${filteredUsed.length}):</div>
2375                        <div style="max-height: 350px; overflow-y: auto;" id="wb-used-queries-container">
2376                            ${filteredUsed.length > 0 ? filteredUsed.map(query => `
2377                                <div class="wb-desc-query-item used">
2378                                    <span class="wb-desc-query-text">${highlightWords(query, currentMinusWords, true, 'used')}</span>
2379                                    <div style="display: flex; align-items: center; gap: 8px;">
2380                                        <span class="wb-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
2381										<span class="wb-desc-query-exclude" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #991b1b; font-weight: bold;" title="Исключить запрос">×</span>
2382                                    </div>
2383                                </div>
2384                            `).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
2385                        </div>
2386                    </div>
2387                    
2388                    <div>
2389                        <div style="margin-bottom: 12px; font-weight: 600; color: #6b7280;">⬜ Неиспользованные (${filteredUnused.length}):</div>
2390                        <div style="max-height: 350px; overflow-y: auto;" id="wb-unused-queries-container">
2391                            ${filteredUnused.length > 0 ? filteredUnused.map(query => {
2392        const isRecoverable = recoverableQueries.includes(query);
2393        const reason = recoverableReasons[query] || '';
2394        return `
2395                                    <div class="wb-desc-query-item unused ${isRecoverable ? 'recoverable' : ''}" ${reason ? `title="${reason}"` : ''}>
2396                                        <span class="wb-desc-query-text">
2397                                            ${isRecoverable ? '⚠️ ' : ''}${highlightWords(query, currentMinusWords, true, 'unused')}
2398                                        </span>
2399                                        <div style="display: flex; align-items: center; gap: 8px;">
2400                                            <span class="wb-desc-query-popularity">${formatNumber(analysis.queryPopularity[query.toLowerCase()] || 0)}</span>
2401                                            <span class="wb-desc-query-include" data-query="${query}" style="cursor: pointer; font-size: 18px; color: #059669; font-weight: bold;" title="Включить запрос">+</span>
2402                                        </div>
2403                                    </div>
2404                                `;
2405    }).join('') : '<div style="padding: 12px; color: #6b7280; text-align: center;">Нет результатов</div>'}
2406                        </div>
2407                    </div>
2408                </div>
2409                
2410                ${recoverableQueries.length > 0 ? `
2411                <div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #fef3c7, #fde68a); border-radius: 8px; border: 1px solid #f59e0b;">
2412                    <div style="font-weight: 600; color: #92400e; margin-bottom: 4px;">
2413                        ⚠️ AI обнаружил ${recoverableQueries.length} пропущенных запросов
2414                    </div>
2415                    <div style="font-size: 13px; color: #78350f;">
2416                        Эти запросы помечены значком ⚠️. Наведите курсор для просмотра причины.
2417                    </div>
2418                </div>
2419                ` : ''}
2420                
2421                <div class="wb-desc-buttons">
2422                    <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-analytics-btn">Закрыть</button>
2423                    <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-with-exclusions-btn">🔄 Перегенерировать с изменениями</button>
2424                </div>
2425            </div>
2426        `;
2427            
2428            setTimeout(() => {
2429                const newUsedContainer = document.getElementById('wb-used-queries-container');
2430                const newUnusedContainer = document.getElementById('wb-unused-queries-container');
2431                if (newUsedContainer) newUsedContainer.scrollTop = usedScrollTop;
2432                if (newUnusedContainer) newUnusedContainer.scrollTop = unusedScrollTop;
2433            }, 0);
2434            
2435            attachEventHandlers();
2436        }
2437    
2438        function highlightWords(text, words, clickable = false, type = 'unused') {
2439            if (!clickable) return text;
2440        
2441            const textWords = text.split(/\s+/);
2442            return textWords.map(word => {
2443                const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '');
2444                return `<span class="wb-desc-query-word" data-word="${cleanWord}" data-type="${type}">${word}</span>`;
2445            }).join(' ');
2446        }
2447
2448        function removeQueriesWithMinusWord(minusWord) {
2449            const minusWordLower = minusWord.toLowerCase();
2450            
2451            usedQueries = usedQueries.filter(query => 
2452                !query.toLowerCase().includes(minusWordLower)
2453            );
2454            
2455            unusedQueries = unusedQueries.filter(query => 
2456                !query.toLowerCase().includes(minusWordLower)
2457            );
2458        }
2459    
2460        function attachEventHandlers() {
2461            const analyticsModal = document.querySelector('.wb-desc-analytics-modal');
2462            if (!analyticsModal) return;
2463            
2464            const searchInput = document.getElementById('wb-analytics-search');
2465            if (searchInput) {
2466                searchInput.addEventListener('input', (e) => {
2467                    searchQuery = e.target.value;
2468                    const cursorPosition = e.target.selectionStart;
2469                    renderModal();
2470                    setTimeout(() => {
2471                        const newSearchInput = document.getElementById('wb-analytics-search');
2472                        if (newSearchInput) {
2473                            newSearchInput.focus();
2474                            newSearchInput.setSelectionRange(cursorPosition, cursorPosition);
2475                        }
2476                    }, 0);
2477                });
2478            }
2479            
2480            analyticsModal.addEventListener('click', (e) => {
2481                if (e.target === analyticsModal) {
2482                    analyticsModal.remove();
2483                }
2484            });
2485            
2486            const closeBtn = document.getElementById('wb-close-analytics-btn');
2487            if (closeBtn) {
2488                closeBtn.addEventListener('click', () => {
2489                    analyticsModal.remove();
2490                });
2491            }
2492            
2493            const regenerateBtn = document.getElementById('wb-regenerate-with-exclusions-btn');
2494            if (regenerateBtn) {
2495                regenerateBtn.addEventListener('click', async () => {
2496                    await GM.setValue('wb_analytics_minus_words', JSON.stringify(currentMinusWords));
2497                    
2498                    const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
2499                    const analyticsData = JSON.parse(analyticsDataStr);
2500                    
2501                    const updatedAnalyticsData = analyticsData.map(data => {
2502                        return {
2503                            keyword: data.keyword,
2504                            queries: data.queries.filter(q => usedQueries.includes(q.query))
2505                        };
2506                    });
2507                    
2508                    await GM.setValue('wb_analytics_data', JSON.stringify(updatedAnalyticsData));
2509                    
2510                    console.log('Wildberries Description Generator: Обновлены данные аналитики для перегенерации');
2511                    console.log('Использованные запросы:', usedQueries.length);
2512                    console.log('Минус-слова:', currentMinusWords);
2513                    
2514                    analyticsModal.remove();
2515                    await regenerateWithExclusions();
2516                });
2517            }
2518            
2519            analyticsModal.querySelectorAll('.wb-desc-minus-word-chip').forEach(chip => {
2520                chip.addEventListener('click', () => {
2521                    const word = chip.dataset.word;
2522                    currentMinusWords = currentMinusWords.filter(w => w !== word);
2523                    console.log(`Wildberries Description Generator: Минус-слово "${word}" удалено`);
2524                    renderModal();
2525                });
2526            });
2527            
2528            analyticsModal.querySelectorAll('.wb-desc-query-exclude').forEach(excludeBtn => {
2529                excludeBtn.addEventListener('click', () => {
2530                    const query = excludeBtn.dataset.query;
2531                    console.log(`Wildberries Description Generator: Перемещаем запрос "${query}" в неиспользованные`);
2532                    
2533                    usedQueries = usedQueries.filter(q => q !== query);
2534                    if (!unusedQueries.includes(query)) {
2535                        unusedQueries.push(query);
2536                    }
2537                    
2538                    renderModal();
2539                });
2540            });
2541            
2542            analyticsModal.querySelectorAll('.wb-desc-query-include').forEach(includeBtn => {
2543                includeBtn.addEventListener('click', () => {
2544                    const query = includeBtn.dataset.query;
2545                    console.log(`Wildberries Description Generator: Перемещаем запрос "${query}" в использованные`);
2546                    
2547                    unusedQueries = unusedQueries.filter(q => q !== query);
2548                    if (!usedQueries.includes(query)) {
2549                        usedQueries.push(query);
2550                    }
2551                    
2552                    const queryLower = query.toLowerCase();
2553                    currentMinusWords = currentMinusWords.filter(w => w !== queryLower);
2554                    
2555                    renderModal();
2556                });
2557            });
2558            
2559            analyticsModal.querySelectorAll('.wb-desc-query-word').forEach(wordSpan => {
2560                wordSpan.addEventListener('click', () => {
2561                    const word = wordSpan.dataset.word;
2562                    
2563                    console.log(`Wildberries Description Generator: Добавляем минус-слово "${word}"`);
2564                    
2565                    if (!currentMinusWords.includes(word)) {
2566                        currentMinusWords.push(word);
2567                    }
2568                    
2569                    removeQueriesWithMinusWord(word);
2570                    
2571                    renderModal();
2572                });
2573            });
2574        }
2575    
2576        const analyticsModal = document.createElement('div');
2577        analyticsModal.className = 'wb-desc-analytics-modal';
2578        document.body.appendChild(analyticsModal);
2579        
2580        renderModal();
2581    }
2582
2583    // ============================================
2584    // ПЕРЕГЕНЕРАЦИЯ С ИСКЛЮЧЕНИЯМИ
2585    // ============================================
2586
2587    async function regenerateWithExclusions() {
2588        console.log('Wildberries Description Generator: Перегенерация с исключениями');
2589    
2590        const analyticsMinusWordsStr = await GM.getValue('wb_analytics_minus_words', '[]');
2591        const analyticsMinusWords = JSON.parse(analyticsMinusWordsStr);
2592    
2593        console.log('Wildberries Description Generator: Минус-слова из аналитики для перегенерации:', analyticsMinusWords);
2594    
2595        const existingModal = document.querySelector('.wb-desc-modal');
2596        if (existingModal) {
2597            console.log('Wildberries Description Generator: Модальное окно уже открыто');
2598    
2599            const minusWordsInput = document.getElementById('wb-minus-words-input');
2600            if (minusWordsInput) {
2601                const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
2602                const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
2603                minusWordsInput.value = allMinusWords.join('\n');
2604    
2605                console.log('Wildberries Description Generator: Обновлены минус-слова в модальном окне:', allMinusWords);
2606            }
2607    
2608            generateDescription(existingModal, true);
2609            return;
2610        }
2611    
2612        await openModal();
2613        await new Promise(resolve => setTimeout(resolve, 100));
2614    
2615        const minusWordsInput = document.getElementById('wb-minus-words-input');
2616        if (minusWordsInput) {
2617            const currentMinusWords = minusWordsInput.value.split('\n').map(k => k.trim()).filter(k => k);
2618            const allMinusWords = [...new Set([...currentMinusWords, ...analyticsMinusWords])];
2619            minusWordsInput.value = allMinusWords.join('\n');
2620    
2621            console.log('Wildberries Description Generator: Обновлены минус-слова в новом модальном окне:', allMinusWords);
2622        }
2623    
2624        const newModal = document.querySelector('.wb-desc-modal');
2625        if (newModal) {
2626            generateDescription(newModal, true);
2627        }
2628    }
2629
2630    // ============================================
2631    // ВСТАВКА ОПИСАНИЯ
2632    // ============================================
2633
2634    async function insertDescription(modal) {
2635        console.log('Wildberries Description Generator: Вставка описания');
2636    
2637        try {
2638            const description = await GM.getValue('wb_generated_description', '');
2639        
2640            if (!description) {
2641                alert('Описание не найдено. Пожалуйста, сгенерируйте описание сначала.');
2642                return;
2643            }
2644        
2645            const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
2646        
2647            if (!descriptionTextarea) {
2648                alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
2649                return;
2650            }
2651        
2652            descriptionTextarea.value = description;
2653            descriptionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
2654            descriptionTextarea.dispatchEvent(new Event('change', { bubbles: true }));
2655        
2656            console.log('Wildberries Description Generator: Описание успешно вставлено');
2657        
2658            modal.remove();
2659        
2660            alert('✅ Описание успешно вставлено!');
2661        
2662        } catch (error) {
2663            console.error('Wildberries Description Generator: Ошибка при вставке описания:', error);
2664            alert('Ошибка при вставке описания. Попробуйте еще раз.');
2665        }
2666    }
2667
2668    // ============================================
2669    // АВТОГЕНЕРАЦИЯ ОПИСАНИЙ ДЛЯ ВСЕХ ТОВАРОВ
2670    // ============================================
2671
2672    function createAutoGenerationButton() {
2673        console.log('Wildberries Description Generator: Создание кнопки автогенерации');
2674        
2675        if (document.querySelector('.wb-auto-btn')) {
2676            console.log('Wildberries Description Generator: Кнопка автогенерации уже добавлена');
2677            return;
2678        }
2679        
2680        const button = document.createElement('button');
2681        button.className = 'wb-auto-btn';
2682        button.textContent = '🤖 Авто описание';
2683        button.addEventListener('click', startAutoGeneration);
2684        
2685        document.body.appendChild(button);
2686        console.log('Wildberries Description Generator: Кнопка автогенерации добавлена');
2687    }
2688    
2689    function getProductsFromPage() {
2690        console.log('Wildberries Description Generator: Получение списка товаров');
2691        
2692        const products = [];
2693        const rows = document.querySelectorAll('table tbody tr, [data-testid="product-row"], .product-item');
2694        
2695        console.log(`Wildberries Description Generator: Найдено строк: ${rows.length}`);
2696        
2697        rows.forEach((row, index) => {
2698            try {
2699                let nmID = null;
2700                
2701                const nmIDElement = row.querySelector('[data-testid="card-nmID-text"]');
2702                if (nmIDElement) {
2703                    const text = nmIDElement.textContent.trim();
2704                    const match = text.match(/Артикул\s+WB:\s*(\d+)/i);
2705                    if (match) {
2706                        nmID = match[1];
2707                        console.log(`Wildberries Description Generator: Найден артикул по data-testid: ${nmID}`);
2708                    }
2709                }
2710                
2711                if (!nmID) {
2712                    const link = row.querySelector('a[href*="nmID="]');
2713                    if (link) {
2714                        const match = link.href.match(/nmID=(\d+)/);
2715                        if (match) nmID = match[1];
2716                    }
2717                }
2718                
2719                if (!nmID) {
2720                    nmID = row.dataset.nmid || row.dataset.productId || row.dataset.id;
2721                }
2722                
2723                if (!nmID) {
2724                    const cells = row.querySelectorAll('td');
2725                    cells.forEach(cell => {
2726                        const text = cell.textContent.trim();
2727                        if (/^\d{8,9}$/.test(text)) {
2728                            nmID = text;
2729                        }
2730                    });
2731                }
2732                
2733                if (nmID) {
2734                    let title = '';
2735                    const titleElement = row.querySelector('[data-testid="card-title-text"]');
2736                    if (titleElement) {
2737                        title = titleElement.textContent.trim();
2738                    }
2739                    
2740                    products.push({
2741                        nmID: nmID,
2742                        title: title || `Товар ${nmID}`,
2743                        rowElement: row
2744                    });
2745                    
2746                    console.log(`Wildberries Description Generator: Найден товар ${nmID}: ${title}`);
2747                }
2748            } catch (e) {
2749                console.error(`Wildberries Description Generator: Ошибка при обработке строки ${index}:`, e);
2750            }
2751        });
2752        
2753        console.log(`Wildberries Description Generator: Всего найдено товаров: ${products.length}`);
2754        return products;
2755    }
2756    
2757    function showProgressModal() {
2758        const modal = document.createElement('div');
2759        modal.id = 'wb-progress-modal';
2760        modal.className = 'wb-progress-modal';
2761        modal.innerHTML = `
2762            <div class="wb-progress-header">🤖 Автогенерация описаний</div>
2763            
2764            <div class="wb-progress-stats">
2765                <div class="wb-progress-stat success">
2766                    <div class="wb-progress-stat-number" id="wb-success-count">0</div>
2767                    <div class="wb-progress-stat-label">Успешно</div>
2768                </div>
2769                <div class="wb-progress-stat error">
2770                    <div class="wb-progress-stat-number" id="wb-error-count">0</div>
2771                    <div class="wb-progress-stat-label">Проблема</div>
2772                </div>
2773            </div>
2774            
2775            <div class="wb-progress-current" id="wb-current-product">
2776                Ожидание...
2777            </div>
2778            
2779            <div id="wb-progress-errors-container" style="display: none;">
2780                <div style="font-weight: 600; margin-bottom: 8px; font-size: 14px; color: #374151;">
2781                    Товары с проблемами (клик для просмотра):
2782                </div>
2783                <div class="wb-progress-errors" id="wb-progress-errors"></div>
2784            </div>
2785            
2786            <div class="wb-progress-buttons">
2787                <button class="wb-progress-btn wb-progress-btn-stop" id="wb-stop-btn">⏹ Остановить</button>
2788                <button class="wb-progress-btn wb-progress-btn-close" id="wb-close-progress-btn" style="display: none;">Закрыть</button>
2789            </div>
2790        `;
2791        
2792        document.body.appendChild(modal);
2793        
2794        document.getElementById('wb-stop-btn').addEventListener('click', () => {
2795            autoGenerationStopped = true;
2796            document.getElementById('wb-stop-btn').disabled = true;
2797            document.getElementById('wb-stop-btn').textContent = '⏳ Остановка...';
2798            document.getElementById('wb-current-product').textContent = 'Остановка процесса...';
2799        });
2800        
2801        document.getElementById('wb-close-progress-btn').addEventListener('click', () => {
2802            modal.remove();
2803        });
2804        
2805        return modal;
2806    }
2807    
2808    function updateProgress(successCount, errorCount, currentProduct, errors) {
2809        const successEl = document.getElementById('wb-success-count');
2810        const errorEl = document.getElementById('wb-error-count');
2811        const currentEl = document.getElementById('wb-current-product');
2812        const errorsContainer = document.getElementById('wb-progress-errors-container');
2813        const errorsList = document.getElementById('wb-progress-errors');
2814        
2815        if (successEl) successEl.textContent = successCount;
2816        if (errorEl) errorEl.textContent = errorCount;
2817        if (currentEl) currentEl.textContent = currentProduct;
2818        
2819        if (errors && errors.length > 0 && errorsContainer && errorsList) {
2820            errorsContainer.style.display = 'block';
2821            errorsList.innerHTML = errors.map(err => `
2822                <div class="wb-progress-error-item" data-nmid="${err.nmID}">
2823                    <div style="font-weight: 600;">${err.title}</div>
2824                    <div style="font-size: 12px; margin-top: 4px;">Артикул: ${err.nmID}</div>
2825                    <div style="font-size: 12px; color: #7f1d1b;">${err.error}</div>
2826                </div>
2827            `).join('');
2828            
2829            errorsList.querySelectorAll('.wb-progress-error-item').forEach(item => {
2830                item.addEventListener('click', () => {
2831                    const nmID = item.dataset.nmid;
2832                    window.open(`https://seller.wildberries.ru/new-goods/card?nmID=${nmID}&type=EXIST_CARD`, '_blank');
2833                });
2834            });
2835        }
2836    }
2837    
2838    async function generateKeywordsForProduct(productInfo) {
2839        console.log('Wildberries Description Generator: Генерация ключевых слов через AI');
2840        
2841        const deduplicate = (list) => {
2842            const seen = new Set();
2843            return list.filter(item => {
2844                const key = item.toLowerCase();
2845                if (seen.has(key)) return false;
2846                seen.add(key);
2847                return true;
2848            });
2849        };
2850        
2851        try {
2852            const prompt = generateMasksPrompt(productInfo);
2853            const response = await RM.aiCall(prompt);
2854            const data = JSON.parse(response);
2855            
2856            const aiKeywords = Array.isArray(data.keywords) ? deduplicate(data.keywords.filter(Boolean)) : [];
2857            if (aiKeywords.length > 0) {
2858                console.log(`Wildberries Description Generator: AI сгенерировал ${aiKeywords.length} ключевых слов из нового промпта`);
2859                return aiKeywords;
2860            }
2861            
2862            const masks = Array.isArray(data.masks) ? deduplicate(data.masks.filter(Boolean)) : [];
2863            if (masks.length > 0) {
2864                console.log(`Wildberries Description Generator: Используем маски (${masks.length}) как ключи`);
2865                return masks.slice(0, 10);
2866            }
2867        } catch (e) {
2868            console.error('Wildberries Description Generator: Ошибка при генерации ключевых слов новым промптом:', e);
2869        }
2870        
2871        const fallbackSource = (productInfo.title || '').toLowerCase();
2872        const words = fallbackSource.split(/\s+/).filter(w => w.length > 3);
2873        return words.slice(0, 5);
2874    }
2875    
2876    async function processProduct(product, progressData) {
2877        console.log(`Wildberries Description Generator: Обработка товара ${product.nmID}`);
2878        
2879        try {
2880            updateProgress(
2881                progressData.successCount,
2882                progressData.errorCount,
2883                `Обработка: ${product.title}`,
2884                progressData.errors
2885            );
2886            
2887            // ТЕСТОВЫЙ РЕЖИМ: проверяем флаг
2888            const testMode = await GM.getValue('wb_test_mode', 'false');
2889            
2890            if (testMode === 'true') {
2891                console.log(`Wildberries Description Generator: ТЕСТОВЫЙ РЕЖИМ - имитация обработки товара ${product.nmID}`);
2892                await new Promise(resolve => setTimeout(resolve, 1000)); // Имитация задержки
2893                
2894                progressData.successCount++;
2895                const processedProducts = await GM.getValue('wb_auto_processed_products', '[]');
2896                const processed = JSON.parse(processedProducts);
2897                processed.push(product.nmID);
2898                await GM.setValue('wb_auto_processed_products', JSON.stringify(processed));
2899                
2900                console.log(`Wildberries Description Generator: ТЕСТОВЫЙ РЕЖИМ - товар ${product.nmID} "обработан"`);
2901                return { success: true };
2902            }
2903            
2904            await GM.setValue('wb_auto_current_product', JSON.stringify(product));
2905            await GM.setValue('wb_auto_mode', 'true');
2906            await GM.setValue('wb_auto_product_status', 'processing');
2907            
2908            const productUrl = `https://seller.wildberries.ru/new-goods/card?nmID=${product.nmID}&type=EXIST_CARD`;
2909            console.log(`Wildberries Description Generator: Открываем товар: ${productUrl}`);
2910            
2911            await GM.openInTab(productUrl, false);
2912            
2913            const maxWaitTime = 300000;
2914            const checkInterval = 2000;
2915            let waitedTime = 0;
2916            
2917            while (waitedTime < maxWaitTime) {
2918                await new Promise(resolve => setTimeout(resolve, checkInterval));
2919                waitedTime += checkInterval;
2920                
2921                if (autoGenerationStopped) {
2922                    throw new Error('Генерация остановлена пользователем');
2923                }
2924                
2925                const status = await GM.getValue('wb_auto_product_status', 'processing');
2926                
2927                if (status === 'completed') {
2928                    console.log(`Wildberries Description Generator: Товар ${product.nmID} успешно обработан`);
2929                    progressData.successCount++;
2930                    
2931                    const processedProducts = await GM.getValue('wb_auto_processed_products', '[]');
2932                    const processed = JSON.parse(processedProducts);
2933                    processed.push(product.nmID);
2934                    await GM.setValue('wb_auto_processed_products', JSON.stringify(processed));
2935                    
2936                    return { success: true };
2937                } else if (status === 'error') {
2938                    const errorMsg = await GM.getValue('wb_auto_product_error', 'Неизвестная ошибка');
2939                    console.error(`Wildberries Description Generator: Ошибка при обработке товара ${product.nmID}:`, errorMsg);
2940                    throw new Error(errorMsg);
2941                }
2942            }
2943            
2944            throw new Error('Превышено время ожидания обработки товара');
2945            
2946        } catch (error) {
2947            console.error(`Wildberries Description Generator: Ошибка при обработке товара ${product.nmID}:`, error);
2948            progressData.errorCount++;
2949            progressData.errors.push({
2950                nmID: product.nmID,
2951                title: product.title,
2952                error: error.message
2953            });
2954            
2955            return { success: false, error: error.message };
2956        } finally {
2957            await GM.setValue('wb_auto_product_status', 'none');
2958            await GM.setValue('wb_auto_mode', 'false');
2959        }
2960    }
2961    
2962    async function startAutoGeneration() {
2963        console.log('Wildberries Description Generator: Запуск автогенерации');
2964        
2965        autoGenerationStopped = false;
2966        
2967        // Проверяем тестовый режим
2968        const testMode = await GM.getValue('wb_test_mode', 'false');
2969        if (testMode === 'true') {
2970            console.log('🧪 ТЕСТОВЫЙ РЕЖИМ АКТИВИРОВАН - описания не будут генерироваться');
2971        }
2972        
2973        // Проверяем стартовый индекс для тестирования
2974        const startFromIndex = await GM.getValue('wb_test_start_index', 0);
2975        if (startFromIndex > 0) {
2976            console.log(`🧪 ТЕСТОВЫЙ РЕЖИМ: Начинаем с товара #${startFromIndex}`);
2977        }
2978        
2979        await GM.setValue('wb_auto_processed_products', JSON.stringify([]));
2980        await GM.setValue('wb_auto_generation_active', 'true');
2981        console.log('Wildberries Description Generator: Список обработанных товаров сброшен');
2982        
2983        showProgressModal();
2984        
2985        const autoBtn = document.querySelector('.wb-auto-btn');
2986        if (autoBtn) autoBtn.disabled = true;
2987        
2988        const progressData = {
2989            successCount: 0,
2990            errorCount: 0,
2991            errors: []
2992        };
2993        
2994        const processedProductsStr = await GM.getValue('wb_auto_processed_products', '[]');
2995        const processedProducts = JSON.parse(processedProductsStr);
2996        
2997        let allProducts = [];
2998        let noNewProductsCount = 0;
2999        let productIndex = 0;
3000        
3001        while (true) {
3002            if (autoGenerationStopped) {
3003                console.log('Wildberries Description Generator: Автогенерация остановлена');
3004                break;
3005            }
3006            
3007            // Получаем текущие товары на странице
3008            const currentProducts = getProductsFromPage();
3009            console.log(`Wildberries Description Generator: Найдено товаров на странице: ${currentProducts.length}`);
3010            
3011            // Добавляем новые товары в общий список (избегаем дубликатов)
3012            for (const product of currentProducts) {
3013                if (!allProducts.find(p => p.nmID === product.nmID)) {
3014                    allProducts.push(product);
3015                }
3016            }
3017            
3018            console.log(`Wildberries Description Generator: Всего уникальных товаров: ${allProducts.length}`);
3019            
3020            // Фильтруем необработанные товары
3021            let productsToProcess = allProducts.filter(p => !processedProducts.includes(p.nmID));
3022            
3023            // Применяем стартовый индекс для тестирования
3024            if (startFromIndex > 0 && productIndex === 0) {
3025                console.log(`🧪 ТЕСТОВЫЙ РЕЖИМ: Пропускаем первые ${startFromIndex} товаров`);
3026                const skippedProducts = productsToProcess.slice(0, startFromIndex);
3027                for (const skipped of skippedProducts) {
3028                    processedProducts.push(skipped.nmID);
3029                }
3030                await GM.setValue('wb_auto_processed_products', JSON.stringify(processedProducts));
3031                productsToProcess = allProducts.filter(p => !processedProducts.includes(p.nmID));
3032                console.log(`🧪 ТЕСТОВЫЙ РЕЖИМ: Осталось обработать ${productsToProcess.length} товаров`);
3033            }
3034            
3035            if (productsToProcess.length === 0) {
3036                console.log('Wildberries Description Generator: Все товары обработаны');
3037                break;
3038            }
3039            
3040            // Обрабатываем первый необработанный товар
3041            const product = productsToProcess[0];
3042            productIndex++;
3043            console.log(`Wildberries Description Generator: Обработка товара #${productIndex}: ${product.nmID} (осталось ${productsToProcess.length})`);
3044            
3045            await processProduct(product, progressData);
3046            
3047            // Обновляем список обработанных товаров
3048            const updatedProcessedStr = await GM.getValue('wb_auto_processed_products', '[]');
3049            const updatedProcessed = JSON.parse(updatedProcessedStr);
3050            processedProducts.push(...updatedProcessed.filter(id => !processedProducts.includes(id)));
3051            
3052            updateProgress(
3053                progressData.successCount,
3054                progressData.errorCount,
3055                `Обработано ${processedProducts.length} товаров, найдено ${allProducts.length} на странице`,
3056                progressData.errors
3057            );
3058            
3059            // Проверяем, нужно ли подгружать ещё товары
3060            if (productsToProcess.length <= 1) {
3061                console.log('Wildberries Description Generator: Пытаемся подгрузить ещё товары через скролл');
3062                
3063                // Сохраняем текущее количество товаров
3064                const beforeScrollCount = allProducts.length;
3065                
3066                // Находим контейнер таблицы с товарами
3067                const tableWrapper = document.querySelector('.Table__wrapper__7g4VfuWpTS');
3068                
3069                if (!tableWrapper) {
3070                    console.log('Wildberries Description Generator: Контейнер таблицы не найден');
3071                    break;
3072                }
3073                
3074                // Пробуем несколько способов скролла
3075                for (let scrollAttempt = 0; scrollAttempt < 5; scrollAttempt++) {
3076                    console.log(`Wildberries Description Generator: Попытка скролла #${scrollAttempt + 1}`);
3077                    
3078                    // Способ 1: Скролл контейнера таблицы к концу
3079                    tableWrapper.scrollTop = tableWrapper.scrollHeight;
3080                    await new Promise(resolve => setTimeout(resolve, 2000));
3081                    
3082                    // Способ 2: Скролл на большое расстояние внутри таблицы
3083                    tableWrapper.scrollBy(0, 5000);
3084                    await new Promise(resolve => setTimeout(resolve, 2000));
3085                    
3086                    // Способ 3: Скролл к последнему товару внутри таблицы
3087                    const lastRow = tableWrapper.querySelector('table tbody tr:last-child');
3088                    if (lastRow) {
3089                        lastRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
3090                        await new Promise(resolve => setTimeout(resolve, 2000));
3091                    }
3092                    
3093                    // Проверяем, появились ли новые товары
3094                    const newProducts = getProductsFromPage();
3095                    console.log(`Wildberries Description Generator: После скролла найдено товаров: ${newProducts.length} (было: ${beforeScrollCount})`);
3096                    
3097                    // Добавляем новые товары
3098                    let addedCount = 0;
3099                    for (const product of newProducts) {
3100                        if (!allProducts.find(p => p.nmID === product.nmID)) {
3101                            allProducts.push(product);
3102                            addedCount++;
3103                        }
3104                    }
3105                    
3106                    if (addedCount > 0) {
3107                        console.log(`Wildberries Description Generator: ✅ Подгружено ${addedCount} новых товаров! Всего: ${allProducts.length}`);
3108                        noNewProductsCount = 0;
3109                        break; // Выходим из цикла скролла, продолжаем обработку
3110                    } else {
3111                        console.log(`Wildberries Description Generator: Новых товаров не появилось (попытка ${scrollAttempt + 1}/5)`);
3112                    }
3113                }
3114                
3115                // Если после всех попыток товары не подгрузились
3116                const afterScrollCount = allProducts.length;
3117                if (afterScrollCount === beforeScrollCount) {
3118                    noNewProductsCount++;
3119                    console.log(`Wildberries Description Generator: Товары не подгрузились после 5 попыток (счётчик: ${noNewProductsCount}/3)`);
3120                    
3121                    if (noNewProductsCount >= 3) {
3122                        console.log('Wildberries Description Generator: Достигнут конец списка товаров');
3123                        break;
3124                    }
3125                }
3126                
3127                // Обновляем список необработанных товаров
3128                productsToProcess = allProducts.filter(p => !processedProducts.includes(p.nmID));
3129                console.log(`Wildberries Description Generator: Необработанных товаров: ${productsToProcess.length}`);
3130            }
3131            
3132            await new Promise(resolve => setTimeout(resolve, 1000));
3133        }
3134        
3135        console.log('Wildberries Description Generator: Автогенерация завершена');
3136        updateProgress(
3137            progressData.successCount,
3138            progressData.errorCount,
3139            `✅ Завершено! Успешно: ${progressData.successCount}, Ошибок: ${progressData.errorCount}`,
3140            progressData.errors
3141        );
3142        
3143        document.getElementById('wb-stop-btn').style.display = 'none';
3144        document.getElementById('wb-close-progress-btn').style.display = 'block';
3145        
3146        if (autoBtn) autoBtn.disabled = false;
3147        
3148        await GM.setValue('wb_auto_generation_active', 'false');
3149        
3150        // Сбрасываем тестовые настройки
3151        if (testMode === 'true') {
3152            console.log('🧪 ТЕСТОВЫЙ РЕЖИМ: Завершён');
3153            await GM.setValue('wb_test_mode', 'false');
3154            await GM.setValue('wb_test_start_index', 0);
3155        }
3156    }
3157    
3158    async function autoProcessProductCard() {
3159        const autoMode = await GM.getValue('wb_auto_mode', 'false');
3160        if (autoMode !== 'true') {
3161            return;
3162        }
3163        
3164        console.log('Wildberries Description Generator: Автоматическая обработка карточки товара');
3165        
3166        const progressIndicator = document.createElement('div');
3167        progressIndicator.id = 'wb-auto-progress-indicator';
3168        progressIndicator.style.cssText = `
3169            position: fixed;
3170            top: 20px;
3171            right: 20px;
3172            background: white;
3173            border-radius: 12px;
3174            padding: 20px;
3175            min-width: 300px;
3176            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
3177            z-index: 10000;
3178            font-family: system-ui, -apple-system, sans-serif;
3179        `;
3180        progressIndicator.innerHTML = `
3181            <div style="font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #001a34;">
3182                🤖 Автогенерация описания
3183            </div>
3184            <div id="wb-auto-stage" style="font-size: 14px; color: #374151; margin-bottom: 12px;">
3185                Инициализация...
3186            </div>
3187            <div style="width: 100%; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
3188                <div id="wb-auto-progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #10b981, #059669); transition: width 0.3s;"></div>
3189            </div>
3190        `;
3191        document.body.appendChild(progressIndicator);
3192        
3193        const updateStage = (stage, progress) => {
3194            const stageEl = document.getElementById('wb-auto-stage');
3195            const progressBar = document.getElementById('wb-auto-progress-bar');
3196            if (stageEl) stageEl.textContent = stage;
3197            if (progressBar) progressBar.style.width = progress + '%';
3198        };
3199        
3200        try {
3201            const currentProductStr = await GM.getValue('wb_auto_current_product', '{}');
3202            const currentProduct = JSON.parse(currentProductStr);
3203            
3204            console.log('Wildberries Description Generator: Текущий товар:', currentProduct);
3205            
3206            updateStage('Загрузка страницы товара...', 5);
3207            await new Promise(resolve => setTimeout(resolve, 8000));
3208            
3209            updateStage('Раскрытие характеристик...', 10);
3210            console.log('Wildberries Description Generator: Раскрываем характеристики');
3211            
3212            let expandButton = document.querySelector('div.Characteristics__expand__570w3PkC7D button');
3213            
3214            if (!expandButton) {
3215                const allButtons = document.querySelectorAll('button');
3216                expandButton = Array.from(allButtons).find(btn => 
3217                    btn.textContent && btn.textContent.trim().toLowerCase().includes('показать все')
3218                );
3219            }
3220            
3221            if (!expandButton) {
3222                const characteristicsSection = document.querySelector('[class*="Characteristics"]');
3223                if (characteristicsSection) {
3224                    expandButton = characteristicsSection.querySelector('button');
3225                }
3226            }
3227            
3228            if (expandButton) {
3229                console.log('Wildberries Description Generator: Нажимаем "Показать все"');
3230                expandButton.click();
3231                console.log('Wildberries Description Generator: Ждем 8 секунд для полного раскрытия характеристик');
3232                await new Promise(resolve => setTimeout(resolve, 8000));
3233            } else {
3234                console.log('Wildberries Description Generator: Кнопка "Показать все" не найдена, ждем 3 секунды');
3235                await new Promise(resolve => setTimeout(resolve, 3000));
3236            }
3237            
3238            updateStage('Сбор информации о товаре...', 15);
3239            const productInfo = getProductInfo();
3240            console.log('Wildberries Description Generator: Информация о товаре получена');
3241            
3242            updateStage('AI генерирует ключевые слова...', 20);
3243            const keywords = await generateKeywordsForProduct(productInfo);
3244            console.log('Wildberries Description Generator: Ключевые слова сгенерированы:', keywords);
3245            
3246            if (keywords.length === 0) {
3247                throw new Error('Не удалось сгенерировать ключевые слова');
3248            }
3249            
3250            updateStage('AI анализирует товар...', 25);
3251            await GM.setValue('wb_keywords_to_process', JSON.stringify(keywords));
3252            await GM.setValue('wb_minus_words', JSON.stringify([]));
3253            await GM.setValue('wb_analytics_data', JSON.stringify([]));
3254            await GM.setValue('wb_collection_status', 'pending');
3255            
3256            console.log('Wildberries Description Generator: AI анализирует товар');
3257            const analysisPrompt = generateProductAnalysisPrompt(productInfo, keywords);
3258
3259            const analysisResponse = await RM.aiCall(analysisPrompt);
3260            const productAnalysis = JSON.parse(analysisResponse);
3261            console.log('Wildberries Description Generator: AI-анализ товара:', productAnalysis);
3262            
3263            await GM.setValue('wb_product_analysis', JSON.stringify(productAnalysis));
3264            await GM.setValue('wb_analytics_minus_words', JSON.stringify([]));
3265            
3266            updateStage('Открытие страницы аналитики...', 30);
3267            console.log('Wildberries Description Generator: Открываем аналитику для сбора данных');
3268            const analyticsUrl = 'https://seller.wildberries.ru/search-analytics/popular-search-queries';
3269            await GM.openInTab(analyticsUrl, false);
3270            
3271            updateStage('Сбор данных из аналитики...', 35);
3272            const maxWaitTime = 300000;
3273            const checkInterval = 2000;
3274            let waitedTime = 0;
3275            
3276            while (waitedTime < maxWaitTime) {
3277                await new Promise(resolve => setTimeout(resolve, checkInterval));
3278                waitedTime += checkInterval;
3279                
3280                const status = await GM.getValue('wb_collection_status', 'pending');
3281                
3282                if (status === 'completed') {
3283                    console.log('Wildberries Description Generator: Данные собраны, генерируем описание');
3284                    break;
3285                } else if (status === 'error') {
3286                    throw new Error('Ошибка при сборе данных из аналитики');
3287                }
3288                
3289                const progress = Math.min(60, 35 + Math.floor((waitedTime / maxWaitTime) * 25));
3290                updateStage(`Сбор данных из аналитики... (${Math.floor(waitedTime / 1000)}с)`, progress);
3291            }
3292            
3293            if (waitedTime >= maxWaitTime) {
3294                throw new Error('Превышено время ожидания сбора данных');
3295            }
3296            
3297            updateStage('Обработка собранных данных...', 65);
3298            const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
3299            const analyticsData = JSON.parse(analyticsDataStr);
3300            
3301            const allQueries = [];
3302            const queryPopularity = {};
3303            analyticsData.forEach(data => {
3304                data.queries.forEach(q => {
3305                    allQueries.push(q.query);
3306                    queryPopularity[q.query.toLowerCase()] = q.popularity;
3307                });
3308            });
3309            
3310            console.log(`Wildberries Description Generator: Собрано ${allQueries.length} запросов`);
3311            
3312            if (allQueries.length === 0) {
3313                throw new Error('Не найдено подходящих запросов для генерации');
3314            }
3315            
3316            const minusWords = [];
3317            
3318            console.log('Wildberries Description Generator: Применяем минус-слова перед генерацией');
3319            console.log('Wildberries Description Generator: Минус-слова:', minusWords);
3320            console.log('Wildberries Description Generator: Запросов до фильтрации:', allQueries.length);
3321            
3322            const filteredQueries = allQueries.filter(query => {
3323                const queryLower = query.toLowerCase();
3324                const hasMinusWord = minusWords.some(minusWord => 
3325                    queryLower.includes(minusWord.toLowerCase())
3326                );
3327    
3328                if (hasMinusWord) {
3329                    console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
3330                }
3331    
3332                return !hasMinusWord;
3333            });
3334            
3335            console.log('Wildberries Description Generator: Запросов после фильтрации:', filteredQueries.length);
3336            
3337            if (filteredQueries.length === 0) {
3338                throw new Error('Все запросы отфильтрованы минус-словами. Попробуйте уменьшить количество минус-слов.');
3339            }
3340            
3341            updateStage('AI генерирует описание...', 70);
3342            console.log('Wildberries Description Generator: Генерируем описание');
3343            
3344            const descriptionPrompt = generateDescriptionPrompt(productInfo, keywords, filteredQueries, queryPopularity, '');
3345            const description = await RM.aiCall(descriptionPrompt);
3346
3347            await GM.setValue('wb_generated_description', description);
3348            
3349            updateStage('Постобработка текста...', 85);
3350            let cleanedDescription = description;
3351            const englishToRussian = {
3352                'crucial': 'ключевой',
3353                'Crucial': 'Ключевой',
3354                'essential': 'важный',
3355                'Essential': 'Важный',
3356                'vital': 'жизненно важный',
3357                'Vital': 'Жизненно важный',
3358                'key': 'ключевой',
3359                'Key': 'Ключевой',
3360                'important': 'важный',
3361                'Important': 'Важный',
3362                'testosterone': 'тестостерон',
3363                'Testosterone': 'Тестостерон',
3364                'energy': 'энергия',
3365                'Energy': 'Энергия'
3366            };
3367
3368            Object.entries(englishToRussian).forEach(([eng, rus]) => {
3369                const regex = new RegExp('\\b' + eng + '\\b', 'g');
3370                cleanedDescription = cleanedDescription.replace(regex, rus);
3371            });
3372
3373            console.log('Wildberries Description Generator: Постобработка завершена');
3374            
3375            updateStage('Вставка описания в карточку...', 90);
3376            console.log('Wildberries Description Generator: Вставляем описание');
3377            const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
3378            if (!descriptionTextarea) {
3379                throw new Error('Не удалось найти поле описания');
3380            }
3381            
3382            descriptionTextarea.value = cleanedDescription;
3383            descriptionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
3384            descriptionTextarea.dispatchEvent(new Event('change', { bubbles: true }));
3385            
3386            updateStage('Сохранение товара...', 95);
3387            console.log('Wildberries Description Generator: Сохраняем товар');
3388            const saveButton = document.querySelector('button[data-testid="save-card-button-primary"]');
3389            if (saveButton) {
3390                await GM.setValue('wb_auto_close_tab', 'true');
3391                
3392                window.addEventListener('beforeunload', () => {
3393                    console.log('Wildberries Description Generator: beforeunload - закрываем вкладку');
3394                    window.close();
3395                });
3396                
3397                const origPushState = history.pushState;
3398                history.pushState = function() {
3399                    console.log('Wildberries Description Generator: history.pushState - закрываем вкладку');
3400                    window.close();
3401                    return origPushState.apply(this, arguments);
3402                };
3403                
3404                saveButton.click();
3405                console.log('Wildberries Description Generator: Кнопка сохранения нажата');
3406                
3407                setTimeout(() => {
3408                    console.log('Wildberries Description Generator: Попытка закрытия #1 (500мс)');
3409                    window.close();
3410                }, 500);
3411                
3412                setTimeout(() => {
3413                    console.log('Wildberries Description Generator: Попытка закрытия #2 (1000мс)');
3414                    window.close();
3415                }, 1000);
3416                
3417                setTimeout(() => {
3418                    console.log('Wildberries Description Generator: Попытка закрытия #3 (1500мс) - финальная');
3419                    window.close();
3420                }, 1500);
3421                
3422                setTimeout(() => {
3423                    console.log('Wildberries Description Generator: Попытка закрытия #4 (2000мс) - финальная');
3424                    window.close();
3425                }, 2000);
3426            } else {
3427                console.log('Wildberries Description Generator: Кнопка сохранения не найдена, пропускаем');
3428            }
3429            
3430            updateStage('✅ Готово! Закрытие вкладки...', 100);
3431            await GM.setValue('wb_auto_product_status', 'completed');
3432            await GM.setValue('wb_auto_mode', 'false');
3433            
3434            console.log('Wildberries Description Generator: Товар успешно обработан, закрываем вкладку');
3435            
3436        } catch (error) {
3437            console.error('Wildberries Description Generator: Ошибка при автоматической обработке товара:', error);
3438            await GM.setValue('wb_auto_product_status', 'error');
3439            await GM.setValue('wb_auto_product_error', error.message);
3440            await GM.setValue('wb_auto_mode', 'false');
3441            
3442            setTimeout(() => {
3443                window.close();
3444            }, 3000);
3445        } finally {
3446            setTimeout(() => {
3447                console.log('Wildberries Description Generator: ПРИНУДИТЕЛЬНОЕ закрытие вкладки');
3448                window.close();
3449            }, 2000);
3450        }
3451    }
3452
3453    // ============================================
3454    // ИНИЦИАЛИЗАЦИЯ
3455    // ============================================
3456
3457    function init() {
3458        console.log('Wildberries Description Generator: Инициализация, URL:', window.location.href);
3459    
3460        if (window.location.href.includes('seller.wildberries.ru/new-goods/card')) {
3461        
3462            GM.getValue('wb_auto_mode', 'false').then(autoMode => {
3463                console.log('Wildberries Description Generator: Режим автогенерации:', autoMode);
3464            
3465                if (autoMode === 'true') {
3466                    console.log('Wildberries Description Generator: Режим автогенерации, запускаем автообработку');
3467                    setTimeout(autoProcessProductCard, 2000);
3468                }
3469            
3470                console.log('Wildberries Description Generator: Добавляем кнопку генератора');
3471                const observer = new MutationObserver((mutations, obs) => {
3472                    const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
3473                    if (descriptionHeader) {
3474                        createGeneratorButton();
3475                        obs.disconnect();
3476                    }
3477                });
3478            
3479                observer.observe(document.body, {
3480                    childList: true,
3481                    subtree: true
3482                });
3483            
3484                setTimeout(createGeneratorButton, 2000);
3485            });
3486        }
3487    
3488        if (window.location.href.includes('seller.wildberries.ru/new-goods/all-goods')) {
3489            console.log('Wildberries Description Generator: Страница списка товаров обнаружена');
3490            
3491            // Создаем кнопку сразу
3492            setTimeout(() => {
3493                console.log('Wildberries Description Generator: Попытка создания кнопки автогенерации');
3494                createAutoGenerationButton();
3495            }, 2000);
3496            
3497            // Также следим за изменениями DOM
3498            const observer = new MutationObserver(debounce(() => {
3499                if (!document.querySelector('.wb-auto-btn')) {
3500                    console.log('Wildberries Description Generator: Кнопка исчезла, пересоздаем');
3501                    createAutoGenerationButton();
3502                }
3503            }, 1000));
3504            
3505            observer.observe(document.body, {
3506                childList: true,
3507                subtree: true
3508            });
3509        
3510            window.addEventListener('beforeunload', async () => {
3511                const generationActive = await GM.getValue('wb_auto_generation_active', 'false');
3512                if (generationActive === 'true') {
3513                    console.log('Wildberries Description Generator: Вкладка со списком закрывается, останавливаем генерацию');
3514                    await GM.setValue('wb_auto_generation_active', 'false');
3515                    await GM.setValue('wb_auto_mode', 'false');
3516                    await GM.setValue('wb_auto_product_status', 'error');
3517                    await GM.setValue('wb_auto_product_error', 'Вкладка со списком товаров была закрыта');
3518                }
3519            });
3520        }
3521    
3522        if (window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
3523            setTimeout(autoCollectOnAnalyticsPage, 2000);
3524        }
3525    }
3526
3527    if (document.readyState === 'loading') {
3528        document.addEventListener('DOMContentLoaded', init);
3529    } else {
3530        init();
3531    }
3532
3533    document.addEventListener('wb-open-generator-modal', () => {
3534        console.log('Wildberries Description Generator: Получено событие открытия модального окна');
3535        openModal();
3536    });
3537
3538})();
Wildberries Description Generator 3.1 | Robomonkey