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