Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
Size
97.4 KB
Version
2.9.5
Created
Jan 15, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Wildberries Description Generator 2.0
3// @description Генератор SEO-описаний для товаров на Wildberries с анализом ключевых слов
4// @version 2.9.5
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: Расширение запущено');
12
13 // Добавляем стили для модального окна
14 TM_addStyle(`
15 .wb-desc-modal {
16 position: fixed;
17 top: 0;
18 left: 0;
19 width: 100%;
20 height: 100%;
21 background: rgba(0, 0, 0, 0.5);
22 display: flex;
23 align-items: center;
24 justify-content: center;
25 z-index: 10000;
26 }
27
28 .wb-desc-modal-content {
29 background: white;
30 border-radius: 12px;
31 padding: 24px;
32 max-width: 600px;
33 width: 90%;
34 max-height: 80vh;
35 overflow-y: auto;
36 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
37 }
38
39 .wb-desc-modal-header {
40 font-size: 22px;
41 font-weight: 600;
42 margin-bottom: 20px;
43 color: #001a34;
44 }
45
46 .wb-desc-input-group {
47 margin-bottom: 16px;
48 }
49
50 .wb-desc-label {
51 display: block;
52 margin-bottom: 8px;
53 font-weight: 500;
54 color: #001a34;
55 font-size: 16px;
56 }
57
58 .wb-desc-textarea {
59 width: 100%;
60 min-height: 100px;
61 padding: 12px;
62 border: 1px solid #d1d5db;
63 border-radius: 8px;
64 font-size: 16px;
65 font-family: inherit;
66 resize: vertical;
67 }
68
69 .wb-desc-textarea:focus {
70 outline: none;
71 border-color: #9333ea;
72 }
73
74 .wb-desc-result {
75 background: #f3f4f6;
76 padding: 16px;
77 border-radius: 8px;
78 margin-bottom: 16px;
79 max-height: 300px;
80 overflow-y: auto;
81 white-space: pre-wrap;
82 word-wrap: break-word;
83 font-size: 16px;
84 }
85
86 .wb-desc-char-count {
87 text-align: right;
88 font-size: 14px;
89 color: #6b7280;
90 margin-top: 4px;
91 }
92
93 .wb-desc-char-count.warning {
94 color: #f59e0b;
95 }
96
97 .wb-desc-char-count.error {
98 color: #ef4444;
99 }
100
101 .wb-desc-buttons {
102 display: flex;
103 gap: 12px;
104 justify-content: flex-end;
105 margin-top: 20px;
106 }
107
108 .wb-desc-btn {
109 padding: 10px 20px;
110 border: none;
111 border-radius: 8px;
112 font-size: 16px;
113 font-weight: 500;
114 cursor: pointer;
115 transition: all 0.2s;
116 }
117
118 .wb-desc-btn-primary {
119 background: #9333ea;
120 color: white;
121 }
122
123 .wb-desc-btn-primary:hover {
124 background: #7e22ce;
125 }
126
127 .wb-desc-btn-primary:disabled {
128 background: #9ca3af;
129 cursor: not-allowed;
130 }
131
132 .wb-desc-btn-secondary {
133 background: #e5e7eb;
134 color: #374151;
135 }
136
137 .wb-desc-btn-secondary:hover {
138 background: #d1d5db;
139 }
140
141 .wb-desc-btn-success {
142 background: #10b981;
143 color: white;
144 }
145
146 .wb-desc-btn-success:hover {
147 background: #059669;
148 }
149
150 .wb-desc-generator-btn {
151 margin-left: 12px;
152 padding: 10px 20px;
153 background: #9333ea;
154 color: white;
155 border: none;
156 border-radius: 8px;
157 font-size: 16px;
158 font-weight: 500;
159 cursor: pointer;
160 transition: all 0.2s;
161 }
162
163 .wb-desc-generator-btn:hover {
164 background: #7e22ce;
165 }
166
167 .wb-desc-status {
168 margin-top: 12px;
169 padding: 8px 12px;
170 border-radius: 6px;
171 font-size: 15px;
172 }
173
174 .wb-desc-status.info {
175 background: #dbeafe;
176 color: #1e40af;
177 }
178
179 .wb-desc-status.success {
180 background: #d1fae5;
181 color: #065f46;
182 }
183
184 .wb-desc-status.error {
185 background: #fee2e2;
186 color: #991b1b;
187 }
188
189 .wb-desc-suggest-btn {
190 margin-top: 8px;
191 padding: 8px 16px;
192 background: #6366f1;
193 color: white;
194 border: none;
195 border-radius: 6px;
196 font-size: 15px;
197 font-weight: 500;
198 cursor: pointer;
199 transition: all 0.2s;
200 }
201
202 .wb-desc-suggest-btn:hover {
203 background: #4f46e5;
204 }
205
206 .wb-desc-suggest-btn:disabled {
207 background: #9ca3af;
208 cursor: not-allowed;
209 }
210
211 .wb-desc-suggested-keywords {
212 margin-top: 12px;
213 padding: 12px;
214 background: #f9fafb;
215 border-radius: 8px;
216 border: 1px solid #e5e7eb;
217 }
218
219 .wb-desc-suggested-header {
220 font-weight: 500;
221 margin-bottom: 8px;
222 color: #374151;
223 font-size: 15px;
224 }
225
226 .wb-desc-checkbox-group {
227 display: grid;
228 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
229 gap: 8px;
230 max-height: 200px;
231 overflow-y: auto;
232 }
233
234 .wb-desc-checkbox-item {
235 display: flex;
236 align-items: center;
237 gap: 8px;
238 }
239
240 .wb-desc-checkbox-item input[type="checkbox"] {
241 cursor: pointer;
242 }
243
244 .wb-desc-checkbox-item label {
245 cursor: pointer;
246 font-size: 15px;
247 color: #374151;
248 margin: 0;
249 }
250
251 .wb-desc-usage-link {
252 color: #6366f1;
253 text-decoration: underline;
254 cursor: pointer;
255 font-size: 14px;
256 }
257
258 .wb-desc-usage-link:hover {
259 color: #4f46e5;
260 }
261
262 .wb-desc-analytics-modal {
263 position: fixed;
264 top: 0;
265 left: 0;
266 width: 100%;
267 height: 100%;
268 background: rgba(0, 0, 0, 0.5);
269 display: flex;
270 align-items: center;
271 justify-content: center;
272 z-index: 10001;
273 }
274
275 .wb-desc-analytics-content {
276 background: white;
277 border-radius: 12px;
278 padding: 24px;
279 max-width: 800px;
280 width: 90%;
281 max-height: 80vh;
282 overflow-y: auto;
283 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
284 }
285
286 .wb-desc-query-item {
287 padding: 8px 12px;
288 margin-bottom: 4px;
289 border-radius: 6px;
290 font-size: 15px;
291 display: flex;
292 justify-content: space-between;
293 align-items: center;
294 }
295
296 .wb-desc-query-item.used {
297 background: #d1fae5;
298 color: #065f46;
299 }
300
301 .wb-desc-query-item.unused {
302 background: #f3f4f6;
303 color: #6b7280;
304 }
305
306 .wb-desc-query-text {
307 flex: 1;
308 }
309
310 .wb-desc-query-popularity {
311 font-weight: 600;
312 margin-left: 12px;
313 }
314
315 .wb-desc-minus-words-section {
316 margin-bottom: 16px;
317 padding: 12px;
318 background: #fef3c7;
319 border-radius: 8px;
320 border: 1px solid #fbbf24;
321 }
322
323 .wb-desc-minus-words-header {
324 font-weight: 600;
325 margin-bottom: 8px;
326 color: #92400e;
327 font-size: 15px;
328 }
329
330 .wb-desc-minus-words-list {
331 display: flex;
332 flex-wrap: wrap;
333 gap: 8px;
334 margin-top: 8px;
335 }
336
337 .wb-desc-minus-word-chip {
338 background: #fbbf24;
339 color: #78350f;
340 padding: 4px 12px;
341 border-radius: 16px;
342 font-size: 14px;
343 display: flex;
344 align-items: center;
345 gap: 6px;
346 }
347
348 .wb-desc-minus-word-remove {
349 cursor: pointer;
350 font-weight: bold;
351 font-size: 16px;
352 }
353
354 .wb-desc-minus-word-remove:hover {
355 color: #991b1b;
356 }
357
358 .wb-desc-query-word {
359 cursor: pointer;
360 padding: 2px 4px;
361 border-radius: 3px;
362 transition: background 0.2s;
363 }
364
365 .wb-desc-query-word:hover {
366 background: #fef3c7;
367 }
368 `);
369
370 // Добавьте в начало скрипта:
371 let lastUrl = location.href;
372
373 // Мониторим изменения URL (для SPA)
374 new MutationObserver(() => {
375 const url = location.href;
376 if (url !== lastUrl) {
377 lastUrl = url;
378 console.log('Wildberries Description Generator: URL изменился');
379
380 // Если перешли на страницу товара - инициализируем
381 if (url.includes('seller.wildberries.ru/new-goods/card')) {
382 setTimeout(init, 1000);
383 }
384 }
385 }).observe(document, { subtree: true, childList: true });
386
387 // Также отслеживаем history API
388 const originalPushState = history.pushState;
389 history.pushState = function() {
390 originalPushState.apply(this, arguments);
391 setTimeout(() => {
392 if (location.href.includes('seller.wildberries.ru/new-goods/card')) {
393 console.log('Wildberries Description Generator: History API навигация');
394 setTimeout(init, 500);
395 }
396 }, 100);
397 };
398
399 // Функция для создания кнопки генератора
400 function createGeneratorButton() {
401 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
402
403 if (!descriptionHeader) {
404 console.log('Wildberries Description Generator: Заголовок описания не найден');
405 return;
406 }
407
408 // Проверяем, не добавлена ли уже кнопка
409 if (document.querySelector('.wb-desc-generator-btn')) {
410 console.log('Wildberries Description Generator: Кнопка уже добавлена');
411 return;
412 }
413
414 const button = document.createElement('button');
415 button.className = 'wb-desc-generator-btn';
416 button.textContent = 'Генератор описаний';
417 button.type = 'button';
418 button.addEventListener('click', function() {
419 // Автоматически открываем ингредиенты
420 const expandButton = document.querySelector('div.Characteristics__expand__570w3PkC7D button');
421 if (expandButton) expandButton.click();
422
423 // Открываем модальное окно
424 setTimeout(openModal, 500);
425 });
426
427 descriptionHeader.appendChild(button);
428 console.log('Wildberries Description Generator: Кнопка добавлена');
429 }
430
431 // Функция для открытия модального окна
432 function openModal() {
433
434 console.log('Wildberries Description Generator: Открытие модального окна');
435
436 const modal = document.createElement('div');
437 modal.className = 'wb-desc-modal';
438 modal.innerHTML = `
439 <div class="wb-desc-modal-content">
440 <div class="wb-desc-modal-header">Генератор описаний для Wildberries</div>
441
442 <div class="wb-desc-input-group">
443 <label class="wb-desc-label">Введите ключевые слова (каждое с новой строки):</label>
444 <textarea class="wb-desc-textarea" id="wb-keywords-input" placeholder="Например: витамины иммунитет здоровье"></textarea>
445 <button class="wb-desc-suggest-btn" id="wb-suggest-keywords-btn">Предложить дополнительные ключи</button>
446 <div id="wb-suggested-keywords-container" style="display: none;"></div>
447 </div>
448
449 <div class="wb-desc-input-group">
450 <label class="wb-desc-label">Минус-слова (каждое с новой строки):</label>
451 <textarea class="wb-desc-textarea" style="min-height: 80px;" id="wb-minus-words-input" placeholder="Например: mixit nivea магнит"></textarea>
452 </div>
453
454 <div id="wb-desc-result-container" style="display: none;">
455 <div class="wb-desc-label">Сгенерированное описание:</div>
456 <div class="wb-desc-result" id="wb-desc-result"></div>
457 <div class="wb-desc-char-count" id="wb-char-count"></div>
458 </div>
459
460 <div id="wb-desc-status-container"></div>
461
462 <div class="wb-desc-buttons">
463 <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-btn">Закрыть</button>
464 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-generate-btn">Сгенерировать</button>
465 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-btn" style="display: none;">Перегенерировать</button>
466 <button class="wb-desc-btn wb-desc-btn-success" id="wb-insert-btn" style="display: none;">Вставить в описание</button>
467 </div>
468 </div>
469 `;
470
471 document.body.appendChild(modal);
472
473 // Обработчики событий
474 modal.addEventListener('click', (e) => {
475 if (e.target === modal) {
476 modal.remove();
477 }
478 });
479
480 document.getElementById('wb-close-btn').addEventListener('click', () => {
481 modal.remove();
482 });
483
484 document.getElementById('wb-suggest-keywords-btn').addEventListener('click', () => {
485 suggestKeywords(modal);
486 });
487
488 document.getElementById('wb-generate-btn').addEventListener('click', () => {
489 generateDescription(modal);
490 });
491
492 document.getElementById('wb-regenerate-btn').addEventListener('click', () => {
493 generateDescription(modal, true);
494 });
495
496 document.getElementById('wb-insert-btn').addEventListener('click', () => {
497 insertDescription(modal);
498 });
499 }
500
501 // Функция для предложения дополнительных ключей
502 async function suggestKeywords(modal) {
503 const keywordsInput = document.getElementById('wb-keywords-input');
504 const suggestBtn = document.getElementById('wb-suggest-keywords-btn');
505 const suggestedContainer = document.getElementById('wb-suggested-keywords-container');
506 const statusContainer = document.getElementById('wb-desc-status-container');
507
508 const keywordsText = keywordsInput.value.trim();
509
510 if (!keywordsText) {
511 showStatus(statusContainer, 'Пожалуйста, сначала введите базовые ключевые слова', 'error');
512 return;
513 }
514
515 const keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
516
517 suggestBtn.disabled = true;
518 showStatus(statusContainer, 'AI анализирует товар и подбирает ключи...', 'info');
519
520 try {
521 // Получаем информацию о товаре
522 const productInfo = getProductInfo();
523
524 // Запрос к AI для предложения ключей
525 const suggestPrompt = `Проанализируй товар и предложи дополнительные ключевые слова для сбора аналитики Wildberries.
526
527ДАННЫЕ О ТОВАРЕ:
528• Название: ${productInfo.title || 'не указано'}
529• Состав: ${productInfo.composition || 'не указан'}
530• Базовые ключи: ${keywords.join(', ')}
531
532ЗАДАЧА:
533Предложи 10-15 дополнительных релевантных ключевых слов для поиска в аналитике Wildberries.
534
535ВАЖНО:
536- Предлагай СИНОНИМЫ и АЛЬТЕРНАТИВНЫЕ названия товара
537- НЕ расширяй базовый ключ (например, если "масло для загара" → НЕ предлагай "сухое масло для загара")
538- Предлагай ДРУГИЕ типы продуктов с тем же назначением
539- Учитывай состав и назначение товара
540- НЕ предлагай бренды или названия конкурентов
541- Ключи должны быть короткими (1-4 слова)
542
543ПРИМЕРЫ ПРАВИЛЬНЫХ предложений:
544- Базовый ключ: "масло для загара" → Предложи: "средство для загара", "лошон для загара", "усилитель загара", "активатор загара", "крем для загара"
545- Базовый ключ: "пенка для умывания" → Предложи: "средство для умывания", "гель для умывания", "для умывания лица", "очищающее средство"
546- Базовый ключ: "тестостерон" → Предложи: "для тестостерона", "повышение тестостерона", "бустер тестостерона", "тестобустер"
547- Базовый ключ: "витамины" → Предложи: "витаминный комплекс", "мультивитамины", "бад", "добавка"
548
549ПРИМЕРЫ НЕПРАВИЛЬНЫХ предложений (НЕ ДЕЛАЙ ТАК):
550- Базовый ключ: "масло для загара" → ❌ "сухое масло для загара", "натуральное масло для загара" (это расширение базового ключа)
551- Базовый ключ: "пенка для умывания" → ❌ "косметика", "уход за кожей", "красота" (слишком общие)
552- Базовый ключ: "тестостерон" → ❌ "здоровье", "бады" (слишком общие)
553
554ВАЖНО:
555- Предлагай ДРУГИЕ способы назвать тот же товар
556- Думай как покупатель: "Как еще можно назвать этот товар?"
557- НЕ добавляй прилагательные к базовому ключу
558
559Верни ТОЛЬКО список ключей в формате JSON:
560{
561 "keywords": ["ключ 1", "ключ 2", "ключ 3", ...]
562}
563
564НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
565
566 console.log('Wildberries Description Generator: Запрос предложений ключей от AI');
567
568 const suggestResponse = await RM.aiCall(suggestPrompt);
569
570 // Парсим ответ
571 let suggestedKeywords = [];
572 try {
573 const suggestData = JSON.parse(suggestResponse);
574 suggestedKeywords = suggestData.keywords || [];
575 console.log(`Wildberries Description Generator: AI предложил ${suggestedKeywords.length} ключей`);
576 } catch (e) {
577 console.error('Wildberries Description Generator: Ошибка парсинга предложенных ключей:', e);
578 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
579 return;
580 }
581
582 if (suggestedKeywords.length === 0) {
583 showStatus(statusContainer, 'AI не смог предложить дополнительные ключи', 'error');
584 return;
585 }
586
587 // Показываем предложенные ключи с чекбоксами
588 suggestedContainer.innerHTML = `
589 <div class="wb-desc-suggested-keywords">
590 <div class="wb-desc-suggested-header">
591 Предложенные ключи (выберите нужные):
592 <button class="wb-desc-suggest-btn" id="wb-toggle-all-btn" style="margin-left: 12px; padding: 4px 12px; font-size: 12px;">Выделить все</button>
593 </div>
594 <div class="wb-desc-checkbox-group">
595 ${suggestedKeywords.map((keyword, index) => `
596 <div class="wb-desc-checkbox-item">
597 <input type="checkbox" id="wb-suggested-${index}" value="${keyword}">
598 <label for="wb-suggested-${index}">${keyword}</label>
599 </div>
600 `).join('')}
601 </div>
602 </div>
603 `;
604
605 suggestedContainer.style.display = 'block';
606
607 // Добавляем обработчик для кнопки "Выделить все"
608 const toggleAllBtn = document.getElementById('wb-toggle-all-btn');
609 toggleAllBtn.addEventListener('click', () => {
610 const checkboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]');
611 const allChecked = Array.from(checkboxes).every(cb => cb.checked);
612
613 checkboxes.forEach(cb => {
614 cb.checked = !allChecked;
615 });
616
617 toggleAllBtn.textContent = allChecked ? 'Выделить все' : 'Снять выделение';
618 });
619
620 showStatus(statusContainer, `AI предложил ${suggestedKeywords.length} дополнительных ключей. Выберите нужные и нажмите "Сгенерировать"`, 'success');
621
622 } catch (error) {
623 console.error('Wildberries Description Generator: Ошибка при предложении ключей:', error);
624 showStatus(statusContainer, 'Ошибка при получении предложений. Попробуйте еще раз.', 'error');
625 } finally {
626 suggestBtn.disabled = false;
627 }
628 }
629
630 // Функция для сбора данных с аналитики
631 async function collectAnalyticsData(keywords, minusWords) {
632 console.log('Wildberries Description Generator: Начало сбора данных с аналитики');
633
634 // Сохраняем данные для доступа из другой вкладки
635 await GM.setValue('wb_keywords_to_process', JSON.stringify(keywords));
636 await GM.setValue('wb_minus_words', JSON.stringify(minusWords));
637 await GM.setValue('wb_analytics_data', JSON.stringify([]));
638 await GM.setValue('wb_collection_status', 'pending');
639
640 // Открываем страницу аналитики в новой вкладке
641 const analyticsUrl = 'https://seller.wildberries.ru/search-analytics/popular-search-queries';
642 await GM.openInTab(analyticsUrl, false);
643
644 console.log('Wildberries Description Generator: Открыта страница аналитики, ожидание сбора данных...');
645
646 // Ждем завершения сбора данных (максимум 5 минут)
647 const maxWaitTime = 300000; // 5 минут
648 const checkInterval = 2000; // проверяем каждые 2 секунды
649 let waitedTime = 0;
650
651 while (waitedTime < maxWaitTime) {
652 await new Promise(resolve => setTimeout(resolve, checkInterval));
653 waitedTime += checkInterval;
654
655 const status = await GM.getValue('wb_collection_status', 'pending');
656
657 if (status === 'completed') {
658 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
659 const analyticsData = JSON.parse(analyticsDataStr);
660 console.log('Wildberries Description Generator: Данные успешно собраны');
661 return analyticsData;
662 } else if (status === 'error') {
663 console.error('Wildberries Description Generator: Ошибка при сборе данных');
664 return [];
665 }
666 }
667
668 console.error('Wildberries Description Generator: Превышено время ожидания сбора данных');
669 return [];
670 }
671
672 // Функция для автоматического сбора данных на странице аналитики
673 async function autoCollectOnAnalyticsPage() {
674 // Проверяем, что мы на странице аналитики
675 if (!window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
676 return;
677 }
678
679 console.log('Wildberries Description Generator: Обнаружена страница аналитики');
680
681 // Проверяем, есть ли задача на сбор данных
682 const status = await GM.getValue('wb_collection_status', 'none');
683 if (status !== 'pending') {
684 return;
685 }
686
687 console.log('Wildberries Description Generator: Начинаем автоматический сбор данных');
688
689 try {
690 const keywordsStr = await GM.getValue('wb_keywords_to_process', '[]');
691 const minusWordsStr = await GM.getValue('wb_minus_words', '[]');
692 const productAnalysisStr = await GM.getValue('wb_product_analysis', '{}');
693 const keywords = JSON.parse(keywordsStr);
694 const minusWords = JSON.parse(minusWordsStr);
695 const productAnalysis = JSON.parse(productAnalysisStr);
696
697 console.log('Wildberries Description Generator: AI-критерии фильтрации:', productAnalysis);
698
699 const analyticsData = [];
700
701 // Ждем загрузки страницы
702 await new Promise(resolve => setTimeout(resolve, 3000));
703
704 for (const keyword of keywords) {
705 console.log(`Wildberries Description Generator: Обработка ключевого слова: ${keyword}`);
706
707 try {
708 // Находим поле поиска
709 const searchInput = document.querySelector('input[name="searchString"]');
710 if (!searchInput) {
711 console.error('Wildberries Description Generator: Поле поиска не найдено');
712 continue;
713 }
714
715 // Очищаем и вводим ключевое слово
716 searchInput.value = '';
717 searchInput.focus();
718 searchInput.value = keyword;
719 searchInput.dispatchEvent(new Event('input', { bubbles: true }));
720 searchInput.dispatchEvent(new Event('change', { bubbles: true }));
721
722 // Ждем загрузки результатов
723 await new Promise(resolve => setTimeout(resolve, 5000));
724
725 // Собираем данные из таблицы
726 const rows = document.querySelectorAll('table tbody tr');
727 const keywordData = {
728 keyword: keyword,
729 queries: []
730 };
731
732 console.log(`Wildberries Description Generator: Найдено строк в таблице: ${rows.length}`);
733
734 rows.forEach(row => {
735 const cells = row.querySelectorAll('td');
736 if (cells.length >= 2) {
737 const query = cells[0]?.textContent?.trim();
738 const popularity = cells[1]?.textContent?.trim();
739
740 if (query && popularity) {
741 const queryLower = query.toLowerCase();
742
743 // 1. Фильтруем по минус-словам
744 const hasMinusWord = minusWords.some(minusWord =>
745 queryLower.includes(minusWord.toLowerCase())
746 );
747
748 if (hasMinusWord) {
749 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит минус-слово)`);
750 return;
751 }
752
753 // 2. Автоматическая фильтрация брендов и стран
754 const autoBanList = [
755 'mixit', 'axis', 'nivea', 'garnier', 'loreal', 'maybelline', 'vichy', 'bioderma',
756 'эвалар', 'солгар', 'now foods', 'доппельгерц', 'артнео', 'гельтек',
757 'корея', 'корейск', 'япония', 'японск', 'франция', 'французск', 'америк', 'китай', 'китайск',
758 'купить', 'цена', 'отзыв', 'инструкция', 'доставка'
759 ];
760
761 const hasAutoBan = autoBanList.some(banned =>
762 queryLower.includes(banned)
763 );
764
765 if (hasAutoBan) {
766 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (автофильтр: бренд/страна)`);
767 return;
768 }
769
770 // 3. УМНАЯ фильтрация английских слов с использованием AI-критериев
771 const hasEnglish = /[a-z]/i.test(query);
772
773 if (hasEnglish) {
774 // Проверяем, разрешено ли это английское слово по AI-критериям
775 const allowedWords = productAnalysis.allowed_english_words || [];
776 const isAllowedEnglish = allowedWords.some(allowed =>
777 queryLower.includes(allowed.toLowerCase())
778 );
779
780 if (!isAllowedEnglish) {
781 // Проверяем, похож ли запрос на базовый ключ (с учетом замены латиницы)
782 const normalizeText = (text) => {
783 const latinToCyrillic = {
784 'a': 'а', 'A': 'А', 'e': 'е', 'E': 'Е', 'o': 'о', 'O': 'О',
785 'p': 'р', 'P': 'Р', 'c': 'с', 'C': 'С', 'y': 'у', 'Y': 'У',
786 'x': 'х', 'X': 'Х', 'k': 'к', 'K': 'К', 'h': 'н', 'H': 'Н',
787 'b': 'в', 'B': 'В', 'm': 'м', 'M': 'М', 't': 'т', 'T': 'Т'
788 };
789 return text.split('').map(char => latinToCyrillic[char] || char).join('').toLowerCase();
790 };
791
792 const normalizedQuery = normalizeText(query);
793 const isSimilarToUserKeyword = keywords.some(k => {
794 const normalizedKeyword = normalizeText(k);
795 return normalizedQuery === normalizedKeyword;
796 });
797
798 if (!isSimilarToUserKeyword) {
799 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (содержит неразрешенные английские слова)`);
800 return;
801 }
802 } else {
803 console.log(`Wildberries Description Generator: Разрешен запрос "${query}" (содержит разрешенное английское слово)`);
804 }
805 }
806
807 // 4. УМНАЯ фильтрация по назначению с использованием AI-критериев
808 const excludedPurposes = productAnalysis.excluded_purposes || [];
809 const hasExcludedPurpose = excludedPurposes.some(excluded =>
810 queryLower.includes(excluded.toLowerCase())
811 );
812
813 if (hasExcludedPurpose) {
814 console.log(`Wildberries Description Generator: Исключен запрос "${query}" (неподходящее назначение по AI-критериям)`);
815 return;
816 }
817
818 // Если все проверки пройдены - добавляем запрос
819 keywordData.queries.push({
820 query,
821 popularity
822 });
823 }
824 }
825 });
826
827 analyticsData.push(keywordData);
828 console.log(`Wildberries Description Generator: Собрано ${keywordData.queries.length} запросов для "${keyword}"`);
829
830 } catch (error) {
831 console.error(`Wildberries Description Generator: Ошибка при обработке ключевого слова "${keyword}":`, error);
832 }
833 }
834
835 // Сохраняем собранные данные
836 await GM.setValue('wb_analytics_data', JSON.stringify(analyticsData));
837 await GM.setValue('wb_collection_status', 'completed');
838
839 console.log('Wildberries Description Generator: Сбор данных завершен, можно закрыть вкладку');
840
841 // Закрываем вкладку через 2 секунды
842 setTimeout(() => {
843 window.close();
844 }, 2000);
845
846 } catch (error) {
847 console.error('Wildberries Description Generator: Ошибка при автоматическом сборе данных:', error);
848 await GM.setValue('wb_collection_status', 'error');
849 }
850 }
851
852 // Функция для получения информации о товаре со страницы
853 function getProductInfo() {
854 console.log('Wildberries Description Generator: Сбор информации о товаре');
855
856 const productInfo = {
857 title: '',
858 currentDescription: '',
859 composition: '',
860 attributes: []
861 };
862
863 // 1. Получаем текущее описание
864 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
865 if (descriptionTextarea) {
866 productInfo.currentDescription = descriptionTextarea.value || '';
867 console.log('Wildberries Description Generator: Текущее описание найдено');
868 }
869
870 // 2. Получаем название товара - ИСПРАВЛЕННЫЙ ПОИСК
871 // Способ 1: По id "editable-title" (contenteditable)
872 const editableTitle = document.querySelector('#editable-title');
873 if (editableTitle) {
874 // Для contenteditable элементов берем textContent или innerText
875 productInfo.title = editableTitle.textContent || editableTitle.innerText || '';
876 console.log('Wildberries Description Generator: Название найдено по #editable-title');
877 }
878
879 // Способ 2: Поиск по label "Наименование"
880 if (!productInfo.title) {
881 // Находим label с текстом "Наименование"
882 const labels = document.querySelectorAll('label, span, div');
883 const nameLabel = Array.from(labels).find(el =>
884 el.textContent && el.textContent.trim() === 'Наименование'
885 );
886
887 if (nameLabel) {
888 console.log('Wildberries Description Generator: Найден label "Наименование"');
889 // Ищем поле ввода рядом с label
890 const parent = nameLabel.closest('div, .field-wrapper, .form-group');
891 if (parent) {
892 // Ищем input или contenteditable
893 const input = parent.querySelector('input, textarea, [contenteditable="true"]');
894 if (input) {
895 if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
896 productInfo.title = input.value || '';
897 } else {
898 productInfo.title = input.textContent || input.innerText || '';
899 }
900 console.log('Wildberries Description Generator: Название найдено рядом с label');
901 }
902 }
903 }
904 }
905
906 // Способ 3: Общий поиск input
907 if (!productInfo.title) {
908 const nameInput = document.querySelector('input[name*="name"], input[placeholder*="Название"], input[placeholder*="наименование"]');
909 if (nameInput) {
910 productInfo.title = nameInput.value || '';
911 console.log('Wildberries Description Generator: Название найдено через общий поиск');
912 }
913 }
914
915 // 3. Получаем состав товара
916 const compositionBlock = document.querySelector('div#Состав');
917 if (compositionBlock) {
918 console.log('Wildberries Description Generator: Найден блок "Состав"');
919
920 let ingredients = [];
921
922 // Способ 1: По data-testid
923 const byTestId = compositionBlock.querySelectorAll('[data-testid^="undefined-select-item-"]');
924 if (byTestId.length > 0) {
925 ingredients = Array.from(byTestId).map(el => el.textContent.trim()).filter(Boolean);
926 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по data-testid`);
927 }
928 // Способ 2: По классу
929 else {
930 const byClass = compositionBlock.querySelectorAll('.Selected-item__text__6P8EDRPmWD');
931 if (byClass.length > 0) {
932 ingredients = Array.from(byClass).map(el => el.textContent.trim()).filter(Boolean);
933 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по классу`);
934 }
935 }
936
937 // Способ 3: Поиск в чипах
938 if (ingredients.length === 0) {
939 const chips = compositionBlock.querySelectorAll('.Selected-item__fIyMG5Li-v, .New-multi-select-input__selected-item__KOh9hWF-7q');
940 chips.forEach(chip => {
941 const text = chip.textContent.trim();
942 const cleanText = text.replace(/remove$/, '').trim();
943 if (cleanText) {
944 ingredients.push(cleanText);
945 }
946 });
947 console.log(`Wildberries Description Generator: Извлечено ${ingredients.length} ингредиентов по чипам`);
948 }
949
950 // Объединяем ингредиенты
951 if (ingredients.length > 0) {
952 productInfo.composition = ingredients.join(', ');
953 console.log('Wildberries Description Generator: Состав товара извлечен');
954 } else {
955 console.log('Wildberries Description Generator: Ингредиенты не найдены в блоке "Состав"');
956 }
957 } else {
958 console.log('Wildberries Description Generator: Блок "Состав" не найден');
959
960 // Старый способ
961 const compositionTextarea = document.querySelector('textarea[placeholder*="Состав"], textarea[name*="composition"]');
962 if (compositionTextarea && compositionTextarea.value) {
963 productInfo.composition = compositionTextarea.value;
964 console.log('Wildberries Description Generator: Состав найден через textarea');
965 }
966 }
967
968 // 4. Дополнительные атрибуты (опционально)
969 try {
970 const attributes = {};
971
972 // Цвет
973 const colorElement = document.querySelector('div#Цвет');
974 if (colorElement) {
975 const colorText = colorElement.querySelector('[data-testid^="undefined-select-item-"]')?.textContent;
976 if (colorText) attributes.color = colorText.trim();
977 }
978
979 // Бренд
980 const brandInput = document.querySelector('input[placeholder*="Бренд"], input[name*="brand"]');
981 if (brandInput && brandInput.value) {
982 attributes.brand = brandInput.value;
983 }
984
985 if (Object.keys(attributes).length > 0) {
986 productInfo.attributes = Object.entries(attributes).map(([key, value]) => `${key}: ${value}`);
987 }
988 } catch (e) {
989 console.log('Wildberries Description Generator: Ошибка при сборе атрибутов:', e);
990 }
991
992 console.log('Wildberries Description Generator: Информация о товаре собрана', {
993 titleLength: productInfo.title.length,
994 descriptionLength: productInfo.currentDescription.length,
995 compositionLength: productInfo.composition.length,
996 attributesCount: productInfo.attributes.length
997 });
998
999 return productInfo;
1000 }
1001
1002 // Функция для анализа использованных ключей и расчета популярности
1003 async function analyzeUsedKeywords(description) {
1004 console.log('Wildberries Description Generator: Анализ использованных ключей');
1005
1006 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
1007 const analyticsData = JSON.parse(analyticsDataStr);
1008
1009 const descriptionLower = description.toLowerCase();
1010 const usedQueries = [];
1011 const usedQueriesSet = new Set(); // Для отслеживания уникальных запросов
1012 let totalPopularity = 0;
1013
1014 // Проходим по всем собранным запросам
1015 analyticsData.forEach(keywordData => {
1016 keywordData.queries.forEach(queryData => {
1017 const query = queryData.query.toLowerCase();
1018
1019 // Проверяем, используется ли запрос И не был ли уже добавлен
1020 if (descriptionLower.includes(query) && !usedQueriesSet.has(query)) {
1021 // Парсим популярность (убираем пробелы)
1022 const popularityStr = queryData.popularity.replace(/\s/g, '');
1023 const popularity = parseInt(popularityStr) || 0;
1024
1025 usedQueries.push({
1026 query: queryData.query,
1027 popularity: popularity
1028 });
1029
1030 usedQueriesSet.add(query); // Добавляем в Set для отслеживания
1031 totalPopularity += popularity;
1032 }
1033 });
1034 });
1035
1036 const totalQueriesAvailable = analyticsData.reduce((sum, kd) => sum + kd.queries.length, 0);
1037
1038 console.log(`Wildberries Description Generator: Использовано ${usedQueries.length} запросов из ${totalQueriesAvailable} доступных`);
1039 console.log(`Wildberries Description Generator: Общая популярность: ${totalPopularity}`);
1040
1041 return {
1042 usedQueries,
1043 totalPopularity,
1044 totalQueriesAvailable
1045 };
1046 }
1047
1048 // Функция для форматирования числа с разделителями
1049 function formatNumber(num) {
1050 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
1051 }
1052
1053 // Функция для показа детальной аналитики использования запросов
1054 async function showUsageAnalytics() {
1055 console.log('Wildberries Description Generator: Открытие детальной аналитики');
1056
1057 const description = await GM.getValue('wb_generated_description', '');
1058 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
1059 const analyticsData = JSON.parse(analyticsDataStr);
1060
1061 if (!description || !analyticsData || analyticsData.length === 0) {
1062 alert('Нет данных для анализа');
1063 return;
1064 }
1065
1066 const descriptionLower = description.toLowerCase();
1067
1068 // Получаем ID текущего товара из URL
1069 const urlParams = new URLSearchParams(window.location.search);
1070 const productId = urlParams.get('nmID') || 'unknown';
1071
1072 // Получаем минус-слова для ТЕКУЩЕГО товара
1073 const analyticsMinusWordsStr = await GM.getValue(`wb_analytics_minus_words_${productId}`, '[]');
1074 let analyticsMinusWords = JSON.parse(analyticsMinusWordsStr);
1075
1076 console.log(`Wildberries Description Generator: Загружены минус-слова для товара ${productId}:`, analyticsMinusWords);
1077
1078 // Получаем текущие минус-слова из аналитики
1079 const analyticsMinusWordsStr2 = await GM.getValue('wb_analytics_minus_words', '[]');
1080 let analyticsMinusWords2 = JSON.parse(analyticsMinusWordsStr2);
1081
1082 console.log('Wildberries Description Generator: Текущие минус-слова из аналитики:', analyticsMinusWords2);
1083
1084 // Объединяем минус-слова
1085 analyticsMinusWords = [...analyticsMinusWords, ...analyticsMinusWords2];
1086 console.log('Wildberries Description Generator: Общие минус-слова:', analyticsMinusWords);
1087
1088 // Собираем все запросы с информацией об использовании
1089 const allQueriesWithUsage = [];
1090 const uniqueQueriesSet = new Set();
1091
1092 analyticsData.forEach(keywordData => {
1093 keywordData.queries.forEach(queryData => {
1094 const query = queryData.query;
1095 const queryLower = query.toLowerCase();
1096
1097 if (uniqueQueriesSet.has(queryLower)) {
1098 return;
1099 }
1100
1101 uniqueQueriesSet.add(queryLower);
1102
1103 const popularityStr = queryData.popularity.replace(/\s/g, '');
1104 const popularity = parseInt(popularityStr) || 0;
1105
1106 const isUsed = descriptionLower.includes(queryLower);
1107
1108 allQueriesWithUsage.push({
1109 query: queryData.query,
1110 popularity: popularity,
1111 isUsed: isUsed,
1112 keyword: keywordData.keyword
1113 });
1114 });
1115 });
1116
1117 // Сортируем: сначала использованные, потом по популярности
1118 allQueriesWithUsage.sort((a, b) => {
1119 if (a.isUsed && !b.isUsed) return -1;
1120 if (!a.isUsed && b.isUsed) return 1;
1121 return b.popularity - a.popularity;
1122 });
1123
1124 const usedCount = allQueriesWithUsage.filter(q => q.isUsed).length;
1125 const unusedCount = allQueriesWithUsage.filter(q => !q.isUsed).length;
1126
1127 // Функция для рендера минус-слов
1128 const renderMinusWords = () => {
1129 const minusWordsSection = document.getElementById('wb-minus-words-section');
1130 if (!minusWordsSection) return;
1131
1132 if (analyticsMinusWords.length === 0) {
1133 minusWordsSection.style.display = 'none';
1134 } else {
1135 minusWordsSection.style.display = 'block';
1136 const minusWordsList = minusWordsSection.querySelector('.wb-desc-minus-words-list');
1137 minusWordsList.innerHTML = analyticsMinusWords.map(word => `
1138 <div class="wb-desc-minus-word-chip" data-word="${word}">
1139 <span>${word}</span>
1140 <span class="wb-desc-minus-word-remove">×</span>
1141 </div>
1142 `).join('');
1143
1144 // Добавляем обработчики удаления
1145 minusWordsList.querySelectorAll('.wb-desc-minus-word-remove').forEach(removeBtn => {
1146 removeBtn.addEventListener('click', async () => {
1147 const chip = removeBtn.closest('.wb-desc-minus-word-chip');
1148 const word = chip.dataset.word;
1149 analyticsMinusWords = analyticsMinusWords.filter(w => w !== word);
1150 await GM.setValue(`wb_analytics_minus_words_${productId}`, JSON.stringify(analyticsMinusWords));
1151 await GM.setValue('wb_analytics_minus_words', JSON.stringify(analyticsMinusWords));
1152 console.log(`Wildberries Description Generator: Добавлено минус-слово: ${word}`);
1153 renderMinusWords();
1154 filterQueriesByMinusWords();
1155 });
1156 });
1157 }
1158 };
1159
1160 // Функция для фильтрации запросов по минус-словам
1161 const filterQueriesByMinusWords = () => {
1162 const queryItems = analyticsModal.querySelectorAll('.wb-desc-query-item');
1163 queryItems.forEach(item => {
1164 const query = item.dataset.query.toLowerCase();
1165 const hasMinusWord = analyticsMinusWords.some(minusWord =>
1166 query.includes(minusWord.toLowerCase())
1167 );
1168
1169 if (hasMinusWord) {
1170 item.style.display = 'none';
1171 } else {
1172 // Проверяем поисковый фильтр
1173 const searchTerm = searchInput.value.toLowerCase();
1174 if (searchTerm && !query.includes(searchTerm)) {
1175 item.style.display = 'none';
1176 } else {
1177 item.style.display = 'flex';
1178 }
1179 }
1180 });
1181 };
1182
1183 // Функция для разбивки запроса на кликабельные слова
1184 const makeQueryClickable = (query) => {
1185 const words = query.split(/\s+/);
1186 return words.map(word => `<span class="wb-desc-query-word" data-word="${word.toLowerCase()}">${word}</span>`).join(' ');
1187 };
1188
1189 // Создаем модальное окно с аналитикой
1190 const analyticsModal = document.createElement('div');
1191 analyticsModal.className = 'wb-desc-analytics-modal';
1192 analyticsModal.innerHTML = `
1193 <div class="wb-desc-analytics-content">
1194 <div class="wb-desc-modal-header">Детальная аналитика использования запросов</div>
1195
1196 <div id="wb-minus-words-section" class="wb-desc-minus-words-section" style="display: none;">
1197 <div class="wb-desc-minus-words-header">Минус-слова (клик по слову в запросе добавляет его сюда):</div>
1198 <div class="wb-desc-minus-words-list"></div>
1199 </div>
1200
1201 <div style="margin-bottom: 16px;">
1202 <input type="text" id="wb-search-queries" placeholder="Поиск по запросам..." style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 15px;">
1203 </div>
1204
1205 <div style="margin-bottom: 16px; font-size: 15px;">
1206 <div style="color: #065f46; font-weight: 600;">✅ Использовано: <span id="wb-used-count">${usedCount}</span> запросов</div>
1207 <div style="color: #6b7280; font-weight: 600;">❌ Не использовано: <span id="wb-unused-count">${unusedCount}</span> запросов</div>
1208 <div style="margin-top: 12px; color: #6b7280; font-size: 14px;">
1209 Нажмите ❌ чтобы исключить запрос, ➕ чтобы добавить, или кликните на слово чтобы добавить в минус-слова
1210 </div>
1211 </div>
1212
1213 <div id="wb-queries-list" style="max-height: 500px; overflow-y: auto;">
1214 ${allQueriesWithUsage.map((item, index) => `
1215 <div class="wb-desc-query-item ${item.isUsed ? 'used' : 'unused'}" data-query="${item.query}" data-index="${index}" data-used="${item.isUsed}">
1216 <div style="display: flex; align-items: center; gap: 8px; flex: 1;">
1217 <button class="wb-query-toggle-btn" style="background: none; border: none; cursor: pointer; font-size: 18px; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">
1218 ${item.isUsed ? '❌' : '➕'}
1219 </button>
1220 <div class="wb-desc-query-text">
1221 ${item.isUsed ? '✅ ' : '❌ '} ${makeQueryClickable(item.query)}
1222 </div>
1223 </div>
1224 <div class="wb-desc-query-popularity">${formatNumber(item.popularity)}</div>
1225 </div>
1226 `).join('')}
1227 </div>
1228
1229 <div class="wb-desc-buttons">
1230 <button class="wb-desc-btn wb-desc-btn-secondary" id="wb-close-analytics-btn">Закрыть</button>
1231 <button class="wb-desc-btn wb-desc-btn-primary" id="wb-regenerate-with-changes-btn" style="display: none;">Перегенерировать</button>
1232 </div>
1233 </div>
1234 `;
1235
1236 document.body.appendChild(analyticsModal);
1237
1238 // Рендерим минус-слова
1239 renderMinusWords();
1240
1241 // Отслеживаем изменения
1242 let hasChanges = false;
1243 const originalState = new Map();
1244 allQueriesWithUsage.forEach((item, index) => {
1245 originalState.set(index, item.isUsed);
1246 });
1247
1248 // Обработчик для поиска
1249 const searchInput = document.getElementById('wb-search-queries');
1250 searchInput.addEventListener('input', (e) => {
1251 filterQueriesByMinusWords();
1252 });
1253
1254 // Обработчик для клика по словам в запросах
1255 analyticsModal.querySelectorAll('.wb-desc-query-word').forEach(wordSpan => {
1256 wordSpan.addEventListener('click', async (e) => {
1257 e.stopPropagation();
1258 const word = wordSpan.dataset.word;
1259
1260 // Добавляем слово в минус-слова если его там еще нет
1261 if (!analyticsMinusWords.includes(word)) {
1262 analyticsMinusWords.push(word);
1263 await GM.setValue(`wb_analytics_minus_words_${productId}`, JSON.stringify(analyticsMinusWords));
1264 console.log(`Wildberries Description Generator: Добавлено минус-слово: ${word}`);
1265 renderMinusWords();
1266 filterQueriesByMinusWords();
1267
1268 // Показываем кнопку перегенерации
1269 hasChanges = true;
1270 document.getElementById('wb-regenerate-with-changes-btn').style.display = 'inline-block';
1271 }
1272 });
1273 });
1274
1275 // Обработчик для кнопок переключения
1276 const regenerateBtn = document.getElementById('wb-regenerate-with-changes-btn');
1277 const usedCountSpan = document.getElementById('wb-used-count');
1278 const unusedCountSpan = document.getElementById('wb-unused-count');
1279
1280 analyticsModal.querySelectorAll('.wb-query-toggle-btn').forEach((btn) => {
1281 btn.addEventListener('click', () => {
1282 const queryItem = btn.closest('.wb-desc-query-item');
1283 const isCurrentlyUsed = queryItem.dataset.used === 'true';
1284 const newUsedState = !isCurrentlyUsed;
1285
1286 // Обновляем состояние
1287 queryItem.dataset.used = newUsedState;
1288
1289 // Обновляем визуал
1290 if (newUsedState) {
1291 queryItem.classList.remove('unused');
1292 queryItem.classList.add('used');
1293 btn.textContent = '❌';
1294 const queryText = queryItem.dataset.query;
1295 queryItem.querySelector('.wb-desc-query-text').innerHTML =
1296 '✅ ' + makeQueryClickable(queryText);
1297 } else {
1298 queryItem.classList.remove('used');
1299 queryItem.classList.add('unused');
1300 btn.textContent = '➕';
1301 const queryText = queryItem.dataset.query;
1302 queryItem.querySelector('.wb-desc-query-text').innerHTML =
1303 '❌ ' + makeQueryClickable(queryText);
1304 }
1305
1306 // Обновляем счетчики
1307 const currentUsedCount = analyticsModal.querySelectorAll('.wb-desc-query-item[data-used="true"]').length;
1308 const currentUnusedCount = allQueriesWithUsage.length - currentUsedCount;
1309 usedCountSpan.textContent = currentUsedCount;
1310 unusedCountSpan.textContent = currentUnusedCount;
1311
1312 // Проверяем, есть ли изменения
1313 hasChanges = false;
1314 analyticsModal.querySelectorAll('.wb-desc-query-item').forEach((item, idx) => {
1315 const currentState = item.dataset.used === 'true';
1316 const originalStateValue = originalState.get(idx);
1317 if (currentState !== originalStateValue) {
1318 hasChanges = true;
1319 }
1320 });
1321
1322 // Показываем/скрываем кнопку перегенерации
1323 if (hasChanges) {
1324 regenerateBtn.style.display = 'inline-block';
1325 } else {
1326 regenerateBtn.style.display = 'none';
1327 }
1328 });
1329 });
1330
1331 // Обработчик для перегенерации с изменениями
1332 regenerateBtn.addEventListener('click', async () => {
1333 // Собираем список запросов для исключения
1334 const excludedQueries = [];
1335
1336 analyticsModal.querySelectorAll('.wb-desc-query-item').forEach((item, index) => {
1337 const query = item.dataset.query;
1338 const isCurrentlyUsed = item.dataset.used === 'true';
1339 const wasOriginallyUsed = originalState.get(index);
1340
1341 // Если был использован, но теперь отключен - исключаем
1342 if (wasOriginallyUsed && !isCurrentlyUsed) {
1343 excludedQueries.push(query);
1344 }
1345 });
1346
1347 // Добавляем запросы с минус-словами в исключенные
1348 allQueriesWithUsage.forEach(item => {
1349 const queryLower = item.query.toLowerCase();
1350 const hasMinusWord = analyticsMinusWords.some(minusWord =>
1351 queryLower.includes(minusWord.toLowerCase())
1352 );
1353
1354 if (hasMinusWord && !excludedQueries.includes(item.query)) {
1355 excludedQueries.push(item.query);
1356 }
1357 });
1358
1359 console.log(`Wildberries Description Generator: Исключено ${excludedQueries.length} запросов (включая минус-слова)`);
1360
1361 // Сохраняем изменения
1362 await GM.setValue('wb_excluded_queries', JSON.stringify(excludedQueries));
1363
1364 // Закрываем аналитику
1365 analyticsModal.remove();
1366
1367 // Запускаем перегенерацию
1368 await regenerateWithExclusions();
1369 });
1370
1371 // Обработчики событий
1372 analyticsModal.addEventListener('click', (e) => {
1373 if (e.target === analyticsModal) {
1374 analyticsModal.remove();
1375 }
1376 });
1377
1378 document.getElementById('wb-close-analytics-btn').addEventListener('click', () => {
1379 analyticsModal.remove();
1380 });
1381 }
1382
1383 // Функция для генерации описания
1384 async function generateDescription(modal, skipDataCollection = false) {
1385 const keywordsInput = document.getElementById('wb-keywords-input');
1386 const minusWordsInput = document.getElementById('wb-minus-words-input');
1387 const generateBtn = document.getElementById('wb-generate-btn');
1388 const regenerateBtn = document.getElementById('wb-regenerate-btn');
1389 const insertBtn = document.getElementById('wb-insert-btn');
1390 const resultContainer = document.getElementById('wb-desc-result-container');
1391 const resultDiv = document.getElementById('wb-desc-result');
1392 const charCountDiv = document.getElementById('wb-char-count');
1393 const statusContainer = document.getElementById('wb-desc-status-container');
1394
1395 const keywordsText = keywordsInput.value.trim();
1396 const minusWordsText = minusWordsInput.value.trim();
1397
1398 if (!keywordsText) {
1399 showStatus(statusContainer, 'Пожалуйста, введите ключевые слова', 'error');
1400 return;
1401 }
1402
1403 let keywords = keywordsText.split('\n').map(k => k.trim()).filter(k => k);
1404 const minusWords = minusWordsText.split('\n').map(k => k.trim().toLowerCase()).filter(k => k);
1405
1406 // Добавляем выбранные дополнительные ключи
1407 const suggestedCheckboxes = document.querySelectorAll('#wb-suggested-keywords-container input[type="checkbox"]:checked');
1408 if (suggestedCheckboxes.length > 0) {
1409 const selectedSuggested = Array.from(suggestedCheckboxes).map(cb => cb.value);
1410 keywords = [...keywords, ...selectedSuggested];
1411 console.log(`Wildberries Description Generator: Добавлено ${selectedSuggested.length} дополнительных ключей`);
1412 }
1413
1414 if (keywords.length === 0) {
1415 showStatus(statusContainer, 'Пожалуйста, введите хотя бы одно ключевое слово', 'error');
1416 return;
1417 }
1418
1419 // Показываем загрузку
1420 generateBtn.disabled = true;
1421 regenerateBtn.style.display = 'none';
1422 insertBtn.style.display = 'none';
1423 resultContainer.style.display = 'none';
1424
1425 try {
1426 // Получаем информацию о товаре
1427 const productInfo = getProductInfo();
1428
1429 let allQueries = [];
1430
1431 // Если это перегенерация - используем уже собранные данные
1432 if (skipDataCollection) {
1433 console.log('Wildberries Description Generator: Перегенерация - используем уже собранные данные');
1434 showStatus(statusContainer, 'Генерация описания с помощью AI...', 'info');
1435
1436 // Получаем уже собранные данные
1437 const analyticsDataStr = await GM.getValue('wb_analytics_data', '[]');
1438 const analyticsData = JSON.parse(analyticsDataStr);
1439
1440 if (analyticsData && analyticsData.length > 0) {
1441 analyticsData.forEach(data => {
1442 data.queries.forEach(q => {
1443 allQueries.push(q.query);
1444 });
1445 });
1446 }
1447
1448 console.log(`Wildberries Description Generator: Используем ${allQueries.length} уже собранных запросов`);
1449 } else {
1450 // Первая генерация - собираем данные
1451 // ШАГ 1: AI анализирует товар ДО сбора данных
1452 showStatus(statusContainer, 'AI анализирует товар и создает критерии фильтрации...', 'info');
1453
1454 // Обнуляем минус-слова из аналитики при новой генерации
1455 await GM.setValue('wb_analytics_minus_words', JSON.stringify([]));
1456 console.log('Wildberries Description Generator: Минус-слова из аналитики обнулены для нового товара');
1457
1458 // AI анализирует товар и создает критерии фильтрации
1459 const analysisPrompt = `Проанализируй товар и создай критерии для умной фильтрации поисковых запросов из аналитики Wildberries.
1460
1461ДАННЫЕ О ТОВАРЕ:
1462• Название: ${productInfo.title || 'не указано'}
1463• Состав: ${productInfo.composition || 'не указан'}
1464• Базовые ключи: ${keywords.join(', ')}
1465
1466ЗАДАЧА:
1467Создай критерии фильтрации, чтобы при сборе данных из аналитики мы НЕ ПОТЕРЯЛИ релевантные запросы.
1468
1469ОПРЕДЕЛИ:
1470
14711. КАТЕГОРИЯ ТОВАРА (одна из):
1472 - косметика_лицо (кремы, сыворотки, маски для лица)
1473 - косметика_волосы (шампуни, маски, масла для волос)
1474 - косметика_тело (кремы для тела, скрабы, масла для тела)
1475 - бад_витамины (витамины, минералы, БАДы)
1476 - бад_спорт (спортивное питание, протеины)
1477 - другое
1478
14792. ЦЕЛЕВАЯ АУДИТОРИЯ:
1480 - для_мужчин / для_женщин / для_детей / универсальный
1481
14823. НАЗНАЧЕНИЕ (основное применение):
1483 - Например: "увлажнение кожи лица", "рост волос", "повышение иммунитета"
1484
14854. КЛЮЧЕВЫЕ КОМПОНЕНТЫ (из состава):
1486 - Список главных активных компонентов
1487
14885. РАЗРЕШЕННЫЕ АНГЛИЙСКИЕ СЛОВА/БРЕНДЫ:
1489 - Если в названии есть английские слова (например, "Elementary"), их НУЖНО разрешить
1490 - Список слов, которые можно оставлять в запросах
1491
14926. ИСКЛЮЧАЕМЫЕ НАЗНАЧЕНИЯ:
1493 - Список назначений, которые НЕ подходят для этого товара
1494 - Например, для "сыворотки для лица" исключить: "сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела"
1495 - ВАЖНО: Оставляй общие запросы без уточнения назначения (например, "сыворотка", "витамины")
1496
1497ПРИМЕРЫ:
1498
1499Товар: "Elementary Сыворотка для лица с витамином С"
1500Состав: "Аскорбиновая кислота, Ниацинамид, Гиалуроновая кислота"
1501
1502ПРАВИЛЬНЫЙ ОТВЕТ:
1503{
1504 "category": "косметика_лицо",
1505 "target_audience": "универсальный",
1506 "purpose": "увлажнение и осветление кожи лица",
1507 "key_components": ["витамин с", "аскорбиновая кислота", "ниацинамид", "гиалуроновая кислота"],
1508 "allowed_english_words": ["elementary"],
1509 "excluded_purposes": ["сыворотка для роста волос", "сыворотка для ресниц", "сыворотка для тела"]
1510}
1511
1512Верни ТОЛЬКО JSON в формате выше. НЕ ПИШИ ничего кроме JSON. Начни ответ сразу с {`;
1513
1514 console.log('Wildberries Description Generator: AI анализирует товар');
1515
1516 const analysisResponse = await RM.aiCall(analysisPrompt);
1517
1518 // Парсим ответ
1519 let productAnalysis;
1520 try {
1521 productAnalysis = JSON.parse(analysisResponse);
1522 console.log('Wildberries Description Generator: AI-анализ товара:', productAnalysis);
1523 } catch (e) {
1524 console.error('Wildberries Description Generator: Ошибка парсинга анализа товара:', e);
1525 showStatus(statusContainer, 'Ошибка при анализе товара. Попробуйте еще раз.', 'error');
1526 return;
1527 }
1528
1529 // Сохраняем анализ для использования при сборе данных
1530 await GM.setValue('wb_product_analysis', JSON.stringify(productAnalysis));
1531
1532 // ШАГ 2: Собираем данные с аналитики с умной фильтрацией
1533 showStatus(statusContainer, 'Сбор данных с аналитики (с умной фильтрацией)...', 'info');
1534
1535 // Собираем данные с аналитики
1536 const analyticsData = await collectAnalyticsData(keywords, minusWords);
1537
1538 // Собираем все запросы из аналитики
1539 if (analyticsData && analyticsData.length > 0) {
1540 analyticsData.forEach(data => {
1541 data.queries.forEach(q => {
1542 allQueries.push(q.query);
1543 });
1544 });
1545 }
1546
1547 console.log(`Wildberries Description Generator: Собрано ${allQueries.length} запросов из аналитики`);
1548
1549 // Проверяем, что данные собраны
1550 if (allQueries.length === 0) {
1551 showStatus(statusContainer, 'Не удалось собрать данные с аналитики. Возможно, страница не загрузилась или изменилась структура. Попробуйте еще раз.', 'error');
1552 return;
1553 }
1554 }
1555
1556 // Проверяем, есть ли исключенные запросы из аналитики
1557 const excludedQueriesStr = await GM.getValue('wb_excluded_queries', '[]');
1558 const excludedQueries = JSON.parse(excludedQueriesStr);
1559
1560 let filteredQueries = allQueries;
1561
1562 if (excludedQueries.length > 0) {
1563 console.log(`Wildberries Description Generator: Исключаем ${excludedQueries.length} запросов по выбору пользователя`);
1564 const excludedLower = excludedQueries.map(q => q.toLowerCase());
1565 filteredQueries = allQueries.filter(q => !excludedLower.includes(q.toLowerCase()));
1566 console.log(`Wildberries Description Generator: После исключения осталось ${filteredQueries.length} запросов`);
1567 }
1568
1569 // Генерируем описание с отфильтрованными запросами
1570 if (!skipDataCollection) {
1571 showStatus(statusContainer, 'Генерация описания с помощью AI...', 'info');
1572 }
1573
1574 const descriptionPrompt = `Создай SEO-описание товара для Wildberries (внутренняя SEO-оптимизация).
1575
1576ДАННЫЕ:
1577• Название: ${productInfo.title || 'не указано'}
1578• Состав: ${productInfo.composition || 'не указан'}
1579• Ключевые слова: ${keywords.join(', ')}
1580
1581ЗАПРОСЫ ДЛЯ ИСПОЛЬЗОВАНИЯ (используй МАКСИМУУ из этого списка):
1582${filteredQueries.map((q, i) => `${i+1}. "${q}"`).join('\\n')}
1583
1584=== ЖЕСТКИЕ ТРЕБОВАНИЯ ===
1585
15861. ОБЪЕМ И СТРУКТУРА:
1587 • 3800-4200 символов, только текст описания
1588 • ЕСТЕСТВЕННАЯ СТРУКТУРА: Введение → Основная часть → Практическая часть → Заключение
1589 • НЕ допускай резких переходов между темами
1590
15912. ИСПОЛЬЗОВАНИЕ ЗАПРОСОВ - КРИТИЧЕСКИ ВАЖНО:
1592 • ОБЯЗАТЕЛЬНО используй базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое
1593 • ТВОЯ ГЛАВНАЯ ЗАДАЧА: Использовать МАКСИМУТ запросов из списка выше
1594 • ЦЕЛЬ: Минимум 80-90% запросов из списка (${Math.floor(filteredQueries.length * 0.8)}-${Math.floor(filteredQueries.length * 0.9)} из ${filteredQueries.length})
1595 • ЭТО НЕ РЕКОМЕНДАЦИЯ - ЭТО ОБЯЗАТЕЛЬНОЕ ТРЕБОВАНИЕ!
1596 • Список запросов выше - это РЕАЛЬНЫЕ поисковые запросы пользователей Wildberries
1597 • Каждый запрос из списка - это возможность попасть в поиск
1598 • ИСПОЛЬЗУЙ запросы ТОЧНО как они написаны, можно только склонять
1599 • Примеры ПРАВИЛЬНОГО использования:
1600 - Запрос "сыворотка для лица от прыщей" → "Эта сыворотка для лица от прыщей содержит..."
1601 - Запрос "сыворотка с ниацинамидом 10%" → "Сыворотка с ниацинамидом 10% помогает..."
1602 - Запрос "средство от акне для подростков" → "Средство от акне для подростков разработано..."
1603 • НЕ БОЙСЯ повторять похожие конструкции - это SEO-текст!
1604 • Плотность: минимум 2.5-3 запроса на 100 символов
1605
16063. ЛОГИКА ЗАМЕНЫ ДЛЯ ОТСУТСТВУЮЩИХ КОМПОНЕНТОВ - КРИТИЧЕСКИ ВАЖНО:
1607 • СНАЧАЛА проверь каждый запрос: есть ли упомянутый компонент в составе?
1608 • Если компонента НЕТ в составе - НЕ используй запрос напрямую!
1609 • ОБЯЗАТЕЛЬНО используй только через логику ЗАМЕНЫ
1610
1611 ПРИМЕРЫ ПРАВИЛЬНОГО использования:
1612
1613 Состав: "Коллаген, Гиалуроновая кислота"
1614 Запрос: "сыворотка с ретинолом" (ретинола НЕТ в составе)
1615 ❌ НЕПРАВИЛЬНО: "Эта сыворотка с ретинолом помогает..."
1616 ✅ ПРАВИЛЬНО: "Сыворотка с коллагеном - отличная замена средствам с ретинолом для антивозрастного ухода"
1617
1618 Состав: "Магний, Витамин B6"
1619 Запрос: "добавка с мелатонином" (мелатонина НЕТ в составе)
1620 ❌ НЕПРАВИЛЬНО: "Добавка с мелатонином улучшает сон..."
1621 ✅ ПРАВИЛЬНО: "Магний может стать альтернативой добавкам с мелатонином для улучшения качества сна"
1622
1623 Состав: "Гиалуроновая кислота, Ниацинамид"
1624 Запрос: "крем с коллагеном" (коллагена НЕТ в составе)
1625 ❌ НЕПРАВИЛЬНО: "Крем с коллагеном увлажняет кожу..."
1626 ✅ ПРАВИЛЬНО: "Средство с гиалуроновой кислотой заменяет кремы с коллагеном для глубокого увлажнения"
1627
1628 ШАБЛОНЫ ДЛЯ ЗАМЕНЫ:
1629 • "[Товар с компонентом ИЗ состава] - отличная замена [запрос с ОТСУТСТВУЮЩИМ компонентом]"
1630 • "[Компонент ИЗ состава] может стать альтернативой [запрос с ОТСУТСТВУЮЩИМ компонентом]"
1631 • "Вместо [запрос с ОТСУТСТВУЮЩИМ компонентом] используйте [товар с компонентом ИЗ состава]"
1632 • "[Товар с компонентом ИЗ состава] заменяет [запрос с ОТСУТСТВУЮЩИМ компонентом]"
1633
1634 ВАЖНО: Это позволяет использовать ВСЕ запросы, но ЧЕСТНО - не обманывая покупателя!
1635
16364. ИСПОЛЬЗОВАНИЕ СОСТАВА:
1637 • ОБЯЗАТЕЛЬНО используй ВСЕ компоненты из состава: ${productInfo.composition}
1638 • Каждый компонент используй 2-4 раз в разных контекстах
1639 • Не перечисляй, а вплетай в повествование
1640 • Объединяй схожие компоненты в группы
1641
16425. ПЛОТНОСТЬ И СВЯЗНОСТЬ:
1643 • Каждые 30-40 символов — новый запрос из списка
1644 • Начинай с самых популярных запросов (они в начале списка)
1645 • Проходи по списку ПОСЛЕДОВАТЕЛЬНО, используя запросы один за другим
1646 • Создавай логические переходы между абзацами
1647 • ВАЖНО: Используй ВСЕ запросы, даже если компонента нет в составе - через логику замены!
1648
16496. СТРУКТУРА ПОВЕСТВОВАНИЯ (разделяй на абзацы):
1650
1651 ЧАСТЬ 1: ВВЕДЕНИЕ (10-15% текста)
1652 • Без заголовка, общее описание товара и его назначения, без слов инновационный, революционный
1653 • Основная проблема, которую решает
1654 • Ключевое преимущество
1655 • ИСПОЛЬЗУЙ 30-40 запросов из списка
1656
1657 ЧАСТЬ 2: ОСНОВНАЯ ЧАСТЬ (60-70% текста)
1658 • Подробно о составе и действии компонентов
1659 • Группировка по темам: восстановление → увлажнение → защита
1660 • Как работает продукт (механизм действия)
1661 • Исследования эффективности, но без утверждений. Например: "Исследования показывают, что ...."
1662 • ИСПОЛЬЗУЙ 80-100 запросов из списка
1663
1664 ЧАСТЬ 3: ПРАКТИЧЕСКАЯ ЧАСТЬ (15-20% текста)
1665 • Для кого подходит (естественный переход через "Благодаря...")
1666 • Когда лучше принимать (время, до / после еды, до / во время тренировок или что то другое) и с какими ещё витаминами сочетается. Не пиши сколько капсул принимать.
1667 • Ожидаемые результаты и преимущества, но без обещаний и эффектов. Например: "Наши покупатели отмечают, что ..."
1668 • ИСПОЛЬЗУЙ 30-40 запросов из списка
1669
1670 ЧАСТЬ 4: ЗАКЛЮЧЕНИЕ (5-10% текста)
1671 • Краткое резюме ключевых преимуществ
1672 • Естественный завершающий акцент без рекомендаций про врачей
1673 • ИСПОЛЬЗУЙ 20-30 запросов из списка
1674
16757. ЗАПРЕТЫ:
1676 ✗ ЛЮБЫЕ английские слова (СТРОГО только русский язык, даже для научных терминов)
1677 ✗ Crucial, essential, vital, key, important, testosterone, energy - переводи на русский: ключевой, важный, существенный, тестостерон, энергия
1678 ✗ Бренды, конкуренты, фамилии, названия компаний
1679 ✗ "Вода": "эликсир", "герой", "ритуал", "скажет спасибо", "настоящий", "буквально"
1680 ✗ Повторы одной фразы (используй синонимы и вариации)
1681 ✗ Инструкционный стиль в конце (никаких "Хранить при температуре...")
1682 ✗ Резкие переходы между темами
1683 ✗ Маркированные списки (•) - используй только текст
1684
16858. ПРОВЕРКА (перед ответом):
1686 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ базовые ключевые слова: ${keywords.join(', ')} - минимум 1 раз каждое (ПРОВЕРЬ!)
1687 ✅ ОБЯЗАТЕЛЬНО использованы ВСЕ компоненты состава: ${productInfo.composition} (ПРОВЕРЬ!)
1688 ✅ Использовано ли минимум ${Math.floor(filteredQueries.length * 0.8)} запросов из ${filteredQueries.length} (ПРОВЕРЬ!)
1689 ✅ Нет английских слов (ПРОВЕРЬ на английские слова!)
1690 ✅ Объем 3800-4200 символов
1691 ✅ Плотность: минимум 2.5 запроса на 100 символов
1692 ✅ Естественная структура повествования
1693 ✅ Логичные переходы между абзацами
1694 ✅ ВСЕ слова переведены на русский (crucial → ключевой, testosterone → тестостерон, energy → энергия)
1695
1696=== НАЧНИ ОПИСАНИЕ СРАЗУ ===
1697
1698НЕ ПИШИ вступлений вроде "Я готов помочь" или "Мне нужны данные".
1699НЕ ПРОСИ дополнительные данные.
1700НЕ ЗАДАВАЙ вопросы.
1701НЕ ИСПОЛЬЗУЙ:
1702✗ Слова революционный, инновационный, инвестируйте
1703✗ ЛЮБЫЕ английские слова (crucial, essential, vital, key, testosterone, energy → переводи на русский)
1704✗ Описание неактивных компонентов (тальк, целлюлоза, стеариновая кислота)
1705✗ Не используй заголовки, вопросы и эмоджи !!!
1706✗ Маркированные списки (•)
1707
1708ПРОСТО СГЕНЕРИРУЙ SEO-ОПИСАНИЕ на основе предоставленных данных.
1709
1710ВАЖНО:
17111. Твоя задача - вплести в текст МАКСИМУТ запросов из списка!
17122. Проходи по списку последовательно и используй каждый запрос!
17133. Если компонента из запроса нет в составе - используй логику ЗАМЕНЫ!
1714
1715ПЕРЕД ОТПРАВКОЙ ПРОВЕРЬ:
17161. Все ли базовые ключевые слова (${keywords.join(', ')}) присутствуют в тексте?
17172. Все ли компоненты состава (${productInfo.composition}) упомянуты?
17183. Использовано ли минимум ${Math.floor(filteredQueries.length * 0.8)} запросов из ${filteredQueries.length}?
17194. Нет ли английских слов?
1720
1721Сгенерируй описание:`;
1722
1723 console.log('Wildberries Description Generator: Генерация описания');
1724
1725 // Генерируем описание с помощью AI
1726 const description = await RM.aiCall(descriptionPrompt);
1727
1728 console.log('Wildberries Description Generator: Описание сгенерировано');
1729
1730 // ЖЕСТКАЯ ПОСТОБРАБОТКА: Заменяем английские слова на русские
1731 let cleanedDescription = description;
1732
1733 // Словарь замен английских слов
1734 const englishToRussian = {
1735 'crucial': 'ключевой',
1736 'Crucial': 'Ключевой',
1737 'essential': 'важный',
1738 'Essential': 'Важный',
1739 'vital': 'жизненно важный',
1740 'Vital': 'Жизненно важный',
1741 'key': 'ключевой',
1742 'Key': 'Ключевой',
1743 'important': 'важный',
1744 'Important': 'Важный',
1745 'testosterone': 'тестостерон',
1746 'Testosterone': 'Тестостерон',
1747 'energy': 'энергия',
1748 'Energy': 'Энергия'
1749 };
1750
1751 // Заменяем все английские слова
1752 Object.entries(englishToRussian).forEach(([eng, rus]) => {
1753 const regex = new RegExp('\\b' + eng + '\\b', 'g');
1754 cleanedDescription = cleanedDescription.replace(regex, rus);
1755 });
1756
1757 console.log('Wildberries Description Generator: Постобработка завершена');
1758
1759 // Анализируем использованные ключи
1760 const analysis = await analyzeUsedKeywords(cleanedDescription);
1761
1762 // Проверяем длину
1763 const charCount = cleanedDescription.length;
1764
1765 // Сохраняем описание
1766 await GM.setValue('wb_generated_description', cleanedDescription);
1767
1768 // Показываем результат
1769 resultDiv.textContent = cleanedDescription;
1770 resultContainer.style.display = 'block';
1771
1772 // Обновляем счетчик символов с информацией о ключах (делаем кликабельным)
1773 const popularityInfo = `${charCount} / 5000 символов | <span class="wb-desc-usage-link" id="wb-usage-link">Использовано ${analysis.usedQueries.length} из ${analysis.totalQueriesAvailable} запросов</span> | Общая популярность: ${formatNumber(analysis.totalPopularity)}`;
1774 charCountDiv.innerHTML = popularityInfo;
1775
1776 // Добавляем обработчик клика на ссылку
1777 const usageLink = document.getElementById('wb-usage-link');
1778 if (usageLink) {
1779 usageLink.addEventListener('click', showUsageAnalytics);
1780 }
1781
1782 // Показываем кнопки
1783 generateBtn.style.display = 'none';
1784 regenerateBtn.style.display = 'inline-block';
1785 insertBtn.style.display = 'inline-block';
1786
1787 showStatus(statusContainer, `Описание успешно сгенерировано! (${charCount} символов)`, 'success');
1788
1789 } catch (error) {
1790 console.error('Wildberries Description Generator: Ошибка при генерации описания:', error);
1791 showStatus(statusContainer, 'Ошибка при генерации описания. Попробуйте еще раз.', 'error');
1792 } finally {
1793 generateBtn.disabled = false;
1794 }
1795 }
1796
1797 // Функция для показа статуса
1798 function showStatus(container, message, type) {
1799 container.innerHTML = `<div class="wb-desc-status ${type}">${message}</div>`;
1800 }
1801
1802 // Функция для перегенерации с исключенными запросами
1803 async function regenerateWithExclusions() {
1804 console.log('Wildberries Description Generator: Перегенерация с исключениями');
1805
1806 // Проверяем, открыто ли уже модальное окно
1807 const existingModal = document.querySelector('.wb-desc-modal');
1808 if (existingModal) {
1809 console.log('Wildberries Description Generator: Модальное окно уже открыто, запускаем генерацию с пропуском сбора данных');
1810 // Если окно уже открыто, просто запускаем генерацию с пропуском сбора данных
1811 generateDescription(existingModal, true);
1812 return;
1813 }
1814
1815 // Открываем модальное окно
1816 openModal();
1817
1818 // Ждем, пока модальное окно откроется
1819 await new Promise(resolve => setTimeout(resolve, 100));
1820
1821 // Автоматически запускаем генерацию с пропуском сбора данных
1822 const newModal = document.querySelector('.wb-desc-modal');
1823 if (newModal) {
1824 generateDescription(newModal, true);
1825 }
1826 }
1827
1828 // Функция для вставки описания
1829 async function insertDescription(modal) {
1830 console.log('Wildberries Description Generator: Вставка описания');
1831
1832 try {
1833 const description = await GM.getValue('wb_generated_description', '');
1834
1835 if (!description) {
1836 alert('Описание не найдено. Пожалуйста, сгенерируйте описание сначала.');
1837 return;
1838 }
1839
1840 // Находим поле описания
1841 const descriptionTextarea = document.querySelector('textarea[data-testid="card-form-main-field-description"]');
1842
1843 if (!descriptionTextarea) {
1844 alert('Не удалось найти поле описания. Убедитесь, что вы находитесь на странице редактирования товара.');
1845 return;
1846 }
1847
1848 // Вставляем описание
1849 descriptionTextarea.value = description;
1850 descriptionTextarea.dispatchEvent(new Event('input', { bubbles: true }));
1851 descriptionTextarea.dispatchEvent(new Event('change', { bubbles: true }));
1852
1853 console.log('Wildberries Description Generator: Описание успешно вставлено');
1854
1855 // Закрываем модальное окно
1856 modal.remove();
1857
1858 alert('Описание успешно вставлено!');
1859
1860 } catch (error) {
1861 console.error('Wildberries Description Generator: Ошибка при вставке описания:', error);
1862 alert('Ошибка при вставке описания. Попробуйте еще раз.');
1863 }
1864 }
1865
1866 // Функция для инициализации расширения
1867 function init() {
1868 console.log('Wildberries Description Generator: Инициализация');
1869
1870 // Проверяем, что мы на странице редактирования товара
1871 if (window.location.href.includes('seller.wildberries.ru/new-goods/card')) {
1872
1873 // Ждем загрузки страницы и добавляем кнопку
1874 const observer = new MutationObserver((mutations, obs) => {
1875 const descriptionHeader = document.querySelector('.Description-header__zK-9sKs8RX');
1876 if (descriptionHeader) {
1877 createGeneratorButton();
1878 obs.disconnect();
1879 }
1880 });
1881
1882 observer.observe(document.body, {
1883 childList: true,
1884 subtree: true
1885 });
1886
1887 // Также пробуем добавить кнопку сразу
1888 setTimeout(createGeneratorButton, 2000);
1889 }
1890
1891 // Проверяем, что мы на странице аналитики и запускаем автосбор
1892 if (window.location.href.includes('seller.wildberries.ru/search-analytics/popular-search-queries')) {
1893 setTimeout(autoCollectOnAnalyticsPage, 2000);
1894 }
1895 }
1896
1897 // Запускаем инициализацию
1898 if (document.readyState === 'loading') {
1899 document.addEventListener('DOMContentLoaded', init);
1900 } else {
1901 init();
1902 }
1903
1904})();