Генератор 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="Например: сыворотка для лица витамин с увлажнение">${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="Например: mixit nivea корея">${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})();