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