AI-powered reply generator for Wildberries seller feedback with customizable tone and style settings
Size
24.1 KB
Version
1.1.1
Created
Dec 2, 2025
Updated
11 days ago
1// ==UserScript==
2// @name Wildberries Review Reply Generator
3// @description AI-powered reply generator for Wildberries seller feedback with customizable tone and style settings
4// @version 1.1.1
5// @match https://*.seller.wildberries.ru/*
6// @icon https://static-basket-02.wbbasket.ru/vol20/root-monorepo/latest/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Wildberries Review Reply Generator - Extension Loaded');
12
13 // ============================================
14 // CONFIGURATION & CONSTANTS
15 // ============================================
16
17 const STORAGE_KEYS = {
18 TONE_SETTINGS: 'wb_reply_tone_settings',
19 ADDITIONAL_INSTRUCTIONS: 'wb_reply_additional_instructions'
20 };
21
22 const DEFAULT_TONE = 'Friendly, professional, short, always thank the customer, do not use emojis, пиши на русском, учитывай контекст для ответа';
23 const DEFAULT_INSTRUCTIONS = `* Если нет имени - просто здоровайся
24* Если отзыв негативный, но нет текста - пиши например "жаль что вам не понравилось, хотелось бы больше информации, напишите нам"
25* Если отзыв положительный, но нет текста - пиши например "спасибо за высокую оценку, рады что вам всё понравилось"
26* Если отзыв про доставку - пиши что мы не влияем на доставку, потому что товар хранится на складах wildberries
27* Учитывай при ответе указанные достоинства, недостатки и комментарий
28* Если указано, что нет эффекта или результата - пиши, что каждый организм индивидуален, у всех разные дефициты, рекомендуем пропить курс и после делать выводы`;
29
30 // ============================================
31 // REVIEW DATA EXTRACTION
32 // ============================================
33
34 /**
35 * Extracts the currently visible/expanded review text from the page
36 * @returns {Object} Object containing review text, customer name, and product info
37 */
38 function extractReviewData() {
39 console.log('Extracting review data from page...');
40
41 // Find the review text - look for the expanded feedback item
42 const reviewTextElement = document.querySelector('.Feedback-text-block__che\\+hHALlA span.Text--body-l__rcq6CWuqor');
43 const reviewText = reviewTextElement ? reviewTextElement.textContent.trim() : null;
44
45 // Find customer name
46 const customerNameElement = document.querySelector('.Extended-feedback-item-info-content__aeFOe4qZKw span.Text--h4-bold__zMXtJ5k8XU');
47 const customerName = customerNameElement ? customerNameElement.textContent.trim() : null;
48
49 // Find product name
50 const productNameElement = document.querySelector('.Extended-article-info-card__p99vZY6cjs a.Browser-link--h3__hnyowivo\\+s');
51 const productName = productNameElement ? productNameElement.textContent.trim().replace(/\s+/g, ' ') : null;
52
53 // Find rating
54 const ratingElement = document.querySelector('.Rating__d1dZ\\+mT4Fv');
55 let rating = null;
56 if (ratingElement) {
57 const ratingText = ratingElement.getAttribute('aria-label') || ratingElement.textContent;
58 rating = ratingText;
59 }
60
61 console.log('Extracted review data:', {
62 reviewText,
63 customerName,
64 productName,
65 rating
66 });
67
68 return {
69 reviewText,
70 customerName,
71 productName,
72 rating,
73 success: reviewText !== null
74 };
75 }
76
77 /**
78 * Inserts the generated reply text into the reply textarea on the page
79 * @param {string} replyText - The generated reply text to insert
80 */
81 function insertReplyIntoPage(replyText) {
82 console.log('Inserting reply into page textarea...');
83
84 // Find the reply textarea
85 const replyTextarea = document.querySelector('textarea#answerText[name="answerText"]');
86
87 if (replyTextarea) {
88 // Set the value
89 replyTextarea.value = replyText;
90
91 // Trigger input event so the page knows the value changed
92 const inputEvent = new Event('input', { bubbles: true });
93 replyTextarea.dispatchEvent(inputEvent);
94
95 // Also trigger change event
96 const changeEvent = new Event('change', { bubbles: true });
97 replyTextarea.dispatchEvent(changeEvent);
98
99 // Focus the textarea so user can see it
100 replyTextarea.focus();
101
102 console.log('Reply inserted successfully');
103 return true;
104 } else {
105 console.error('Reply textarea not found on page');
106 return false;
107 }
108 }
109
110 // ============================================
111 // AI REPLY GENERATION (MOCK)
112 // ============================================
113
114 /**
115 * Generates a reply based on review data and settings
116 * This is a MOCK function - replace with actual AI API call
117 *
118 * TO INTEGRATE YOUR AI API:
119 * 1. Replace the mock response with your API call
120 * 2. Use the 'prompt' variable which contains the full context
121 * 3. Return the AI-generated reply text
122 *
123 * Example with fetch:
124 * const response = await fetch('YOUR_AI_API_ENDPOINT', {
125 * method: 'POST',
126 * headers: { 'Content-Type': 'application/json' },
127 * body: JSON.stringify({ prompt: prompt })
128 * });
129 * const data = await response.json();
130 * return data.reply;
131 */
132 async function generateReply(reviewData, toneSettings, additionalInstructions) {
133 console.log('Generating reply with settings:', { toneSettings, additionalInstructions });
134
135 // Build the prompt that will be sent to AI API
136 const prompt = buildPrompt(reviewData, toneSettings, additionalInstructions);
137
138 console.log('Generated prompt for AI:', prompt);
139
140 // ============================================
141 // MOCK AI RESPONSE - REPLACE THIS SECTION
142 // ============================================
143
144 // Simulate API delay
145 await new Promise(resolve => setTimeout(resolve, 1000));
146
147 // Mock response - generate a realistic reply based on the review
148 let mockReply = '';
149
150 // Generate different responses based on review content
151 const reviewLower = reviewData.reviewText.toLowerCase();
152
153 if (reviewLower.includes('не помог') || reviewLower.includes('не работает') || reviewLower.includes('не эффективн')) {
154 mockReply = `Здравствуйте, ${reviewData.customerName}! Благодарим вас за отзыв. Нам очень жаль, что продукт не оправдал ваших ожиданий. Эффективность может зависеть от индивидуальных особенностей организма. Рекомендуем проконсультироваться со специалистом по поводу применения. Будем рады помочь вам с выбором другого продукта.`;
155 } else if (reviewLower.includes('долго') || reviewLower.includes('доставк') || reviewLower.includes('ждал')) {
156 mockReply = `Здравствуйте, ${reviewData.customerName}! Благодарим за ваш отзыв. Нам очень жаль, что вы долго ждали ваш заказ. К сожалению, мы не можем повлиять на скорость работы доставки, так как товар хранится на складах Wildberries. Надеемся, что наш продукт вам понравится!`;
157 } else if (reviewLower.includes('отличн') || reviewLower.includes('хорош') || reviewLower.includes('понравил')) {
158 mockReply = `Здравствуйте, ${reviewData.customerName}! Благодарим вас за положительный отзыв! Очень рады, что наш продукт вам понравился. Желаем вам здоровья и хорошего настроения!`;
159 } else {
160 mockReply = `Здравствуйте, ${reviewData.customerName}! Благодарим вас за отзыв о товаре "${reviewData.productName}". Ваше мнение очень важно для нас. Если у вас есть вопросы по использованию продукта, мы всегда готовы помочь.`;
161 }
162
163 return mockReply;
164
165 // ============================================
166 // TO USE YOUR AI API, REPLACE THE ABOVE CODE WITH:
167 // ============================================
168 // const response = await fetch('YOUR_AI_API_ENDPOINT', {
169 // method: 'POST',
170 // headers: {
171 // 'Content-Type': 'application/json',
172 // 'Authorization': 'Bearer YOUR_API_KEY'
173 // },
174 // body: JSON.stringify({ prompt: prompt })
175 // });
176 // const data = await response.json();
177 // return data.reply;
178 // ============================================
179 }
180
181 /**
182 * Builds the prompt string for AI API
183 * @param {Object} reviewData - The extracted review data
184 * @param {string} toneSettings - Tone and style settings
185 * @param {string} additionalInstructions - Additional instructions
186 * @returns {string} The complete prompt
187 */
188 function buildPrompt(reviewData, toneSettings, additionalInstructions) {
189 let prompt = 'Ты - представитель службы поддержки продавца на маркетплейсе Wildberries. Твоя задача - написать ответ на отзыв покупателя.\n\n';
190
191 prompt += 'ИНФОРМАЦИЯ О ТОВАРЕ И ОТЗЫВЕ:\n';
192 prompt += `Товар: ${reviewData.productName || 'Не указан'}\n`;
193 if (reviewData.rating) {
194 prompt += `Оценка: ${reviewData.rating}\n`;
195 }
196 prompt += `Имя покупателя: ${reviewData.customerName || 'Покупатель'}\n`;
197 prompt += `Текст отзыва: "${reviewData.reviewText}"\n\n`;
198
199 prompt += `ТРЕБОВАНИЯ К ТОНУ И СТИЛЮ ОТВЕТА:\n${toneSettings}\n\n`;
200
201 if (additionalInstructions && additionalInstructions.trim()) {
202 prompt += `ДОПОЛНИТЕЛЬНЫЕ ИНСТРУКЦИИ:\n${additionalInstructions}\n\n`;
203 }
204
205 prompt += 'ВАЖНО:\n';
206 prompt += '- Напиши ТОЛЬКО текст ответа, без дополнительных пояснений\n';
207 prompt += '- Обращайся к покупателю по имени\n';
208 prompt += '- Будь вежливым и профессиональным\n';
209 prompt += '- Учитывай контекст отзыва и оценку\n';
210 prompt += '- Не используй markdown форматирование\n\n';
211
212 prompt += 'Напиши ответ на отзыв:';
213
214 return prompt;
215 }
216
217 // ============================================
218 // UI CREATION
219 // ============================================
220
221 /**
222 * Creates and injects the extension UI panel
223 */
224 async function createUI() {
225 console.log('Creating extension UI...');
226
227 // Load saved settings
228 const savedTone = await GM.getValue(STORAGE_KEYS.TONE_SETTINGS, DEFAULT_TONE);
229 const savedInstructions = await GM.getValue(STORAGE_KEYS.ADDITIONAL_INSTRUCTIONS, DEFAULT_INSTRUCTIONS);
230
231 // Create the UI container
232 const panel = document.createElement('div');
233 panel.id = 'wb-reply-generator-panel';
234 panel.innerHTML = `
235 <div class="wb-rg-header">
236 <h3>AI Reply Generator</h3>
237 <button id="wb-rg-toggle" class="wb-rg-toggle-btn">−</button>
238 </div>
239 <div class="wb-rg-content">
240 <div class="wb-rg-section">
241 <label for="wb-rg-tone">Tone & Style Settings:</label>
242 <textarea id="wb-rg-tone" rows="3" placeholder="e.g., Friendly, professional, short, always thank the customer, do not use emojis">${savedTone}</textarea>
243 </div>
244
245 <div class="wb-rg-section">
246 <label for="wb-rg-instructions">Additional Instructions:</label>
247 <textarea id="wb-rg-instructions" rows="3" placeholder="Add any templates or special instructions here...">${savedInstructions}</textarea>
248 </div>
249
250 <div class="wb-rg-section">
251 <button id="wb-rg-generate" class="wb-rg-btn-primary">Generate Reply</button>
252 </div>
253
254 <div class="wb-rg-section" id="wb-rg-result-section" style="display: none;">
255 <label for="wb-rg-result">Generated Reply:</label>
256 <textarea id="wb-rg-result" rows="6" readonly></textarea>
257 <div class="wb-rg-status" id="wb-rg-status"></div>
258 </div>
259 </div>
260 `;
261
262 // Add styles
263 const styles = `
264 #wb-reply-generator-panel {
265 position: fixed;
266 top: 80px;
267 right: 20px;
268 width: 400px;
269 background: white;
270 border: 1px solid #ddd;
271 border-radius: 8px;
272 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
273 z-index: 10000;
274 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
275 }
276
277 .wb-rg-header {
278 display: flex;
279 justify-content: space-between;
280 align-items: center;
281 padding: 16px;
282 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
283 color: white;
284 border-radius: 8px 8px 0 0;
285 cursor: move;
286 }
287
288 .wb-rg-header h3 {
289 margin: 0;
290 font-size: 16px;
291 font-weight: 600;
292 }
293
294 .wb-rg-toggle-btn {
295 background: rgba(255,255,255,0.2);
296 border: none;
297 color: white;
298 width: 28px;
299 height: 28px;
300 border-radius: 4px;
301 cursor: pointer;
302 font-size: 20px;
303 line-height: 1;
304 transition: background 0.2s;
305 }
306
307 .wb-rg-toggle-btn:hover {
308 background: rgba(255,255,255,0.3);
309 }
310
311 .wb-rg-content {
312 padding: 16px;
313 max-height: 600px;
314 overflow-y: auto;
315 }
316
317 .wb-rg-content.collapsed {
318 display: none;
319 }
320
321 .wb-rg-section {
322 margin-bottom: 16px;
323 }
324
325 .wb-rg-section label {
326 display: block;
327 margin-bottom: 6px;
328 font-weight: 500;
329 font-size: 13px;
330 color: #333;
331 }
332
333 .wb-rg-section textarea {
334 width: 100%;
335 padding: 10px;
336 border: 1px solid #ddd;
337 border-radius: 6px;
338 font-size: 13px;
339 font-family: inherit;
340 resize: vertical;
341 box-sizing: border-box;
342 transition: border-color 0.2s;
343 }
344
345 .wb-rg-section textarea:focus {
346 outline: none;
347 border-color: #667eea;
348 }
349
350 .wb-rg-section textarea[readonly] {
351 background: #f8f9fa;
352 color: #333;
353 }
354
355 .wb-rg-btn-primary {
356 width: 100%;
357 padding: 12px;
358 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
359 color: white;
360 border: none;
361 border-radius: 6px;
362 font-size: 14px;
363 font-weight: 600;
364 cursor: pointer;
365 transition: transform 0.2s, box-shadow 0.2s;
366 }
367
368 .wb-rg-btn-primary:hover {
369 transform: translateY(-1px);
370 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
371 }
372
373 .wb-rg-btn-primary:active {
374 transform: translateY(0);
375 }
376
377 .wb-rg-btn-primary:disabled {
378 opacity: 0.6;
379 cursor: not-allowed;
380 transform: none;
381 }
382
383 .wb-rg-status {
384 margin-top: 8px;
385 padding: 8px;
386 border-radius: 4px;
387 font-size: 12px;
388 text-align: center;
389 }
390
391 .wb-rg-status.success {
392 background: #d4edda;
393 color: #155724;
394 border: 1px solid #c3e6cb;
395 }
396
397 .wb-rg-status.error {
398 background: #f8d7da;
399 color: #721c24;
400 border: 1px solid #f5c6cb;
401 }
402
403 .wb-rg-status.loading {
404 background: #d1ecf1;
405 color: #0c5460;
406 border: 1px solid #bee5eb;
407 }
408 `;
409
410 TM_addStyle(styles);
411
412 // Insert panel into page
413 document.body.appendChild(panel);
414
415 // Make panel draggable
416 makeDraggable(panel);
417
418 // Add event listeners
419 setupEventListeners();
420
421 console.log('Extension UI created successfully');
422 }
423
424 /**
425 * Makes the panel draggable
426 */
427 function makeDraggable(element) {
428 const header = element.querySelector('.wb-rg-header');
429 let isDragging = false;
430 let currentX;
431 let currentY;
432 let initialX;
433 let initialY;
434
435 header.addEventListener('mousedown', (e) => {
436 if (e.target.id === 'wb-rg-toggle') return;
437 isDragging = true;
438 initialX = e.clientX - element.offsetLeft;
439 initialY = e.clientY - element.offsetTop;
440 });
441
442 document.addEventListener('mousemove', (e) => {
443 if (!isDragging) return;
444 e.preventDefault();
445 currentX = e.clientX - initialX;
446 currentY = e.clientY - initialY;
447 element.style.left = currentX + 'px';
448 element.style.top = currentY + 'px';
449 element.style.right = 'auto';
450 });
451
452 document.addEventListener('mouseup', () => {
453 isDragging = false;
454 });
455 }
456
457 /**
458 * Sets up event listeners for UI interactions
459 */
460 function setupEventListeners() {
461 // Toggle panel collapse/expand
462 const toggleBtn = document.getElementById('wb-rg-toggle');
463 const content = document.querySelector('.wb-rg-content');
464
465 toggleBtn.addEventListener('click', () => {
466 content.classList.toggle('collapsed');
467 toggleBtn.textContent = content.classList.contains('collapsed') ? '+' : '−';
468 });
469
470 // Generate reply button
471 const generateBtn = document.getElementById('wb-rg-generate');
472 generateBtn.addEventListener('click', handleGenerateReply);
473
474 // Auto-save settings on change
475 const toneInput = document.getElementById('wb-rg-tone');
476 const instructionsInput = document.getElementById('wb-rg-instructions');
477
478 toneInput.addEventListener('change', async () => {
479 await GM.setValue(STORAGE_KEYS.TONE_SETTINGS, toneInput.value);
480 console.log('Tone settings saved');
481 });
482
483 instructionsInput.addEventListener('change', async () => {
484 await GM.setValue(STORAGE_KEYS.ADDITIONAL_INSTRUCTIONS, instructionsInput.value);
485 console.log('Additional instructions saved');
486 });
487 }
488
489 /**
490 * Handles the generate reply button click
491 */
492 async function handleGenerateReply() {
493 console.log('Generate reply button clicked');
494
495 const generateBtn = document.getElementById('wb-rg-generate');
496 const resultSection = document.getElementById('wb-rg-result-section');
497 const resultTextarea = document.getElementById('wb-rg-result');
498 const statusDiv = document.getElementById('wb-rg-status');
499 const toneInput = document.getElementById('wb-rg-tone');
500 const instructionsInput = document.getElementById('wb-rg-instructions');
501
502 // Disable button during generation
503 generateBtn.disabled = true;
504 generateBtn.textContent = 'Generating...';
505
506 // Show loading status
507 resultSection.style.display = 'block';
508 statusDiv.className = 'wb-rg-status loading';
509 statusDiv.textContent = 'Generating reply...';
510 resultTextarea.value = '';
511
512 try {
513 // Extract review data from page
514 const reviewData = extractReviewData();
515
516 if (!reviewData.success || !reviewData.reviewText) {
517 throw new Error('Could not find review text on the page. Please make sure a review is opened/expanded.');
518 }
519
520 // Get settings
521 const toneSettings = toneInput.value.trim() || DEFAULT_TONE;
522 const additionalInstructions = instructionsInput.value.trim();
523
524 // Save settings
525 await GM.setValue(STORAGE_KEYS.TONE_SETTINGS, toneSettings);
526 await GM.setValue(STORAGE_KEYS.ADDITIONAL_INSTRUCTIONS, additionalInstructions);
527
528 // Generate reply (mock for now)
529 const generatedReply = await generateReply(reviewData, toneSettings, additionalInstructions);
530
531 // Display result
532 resultTextarea.value = generatedReply;
533
534 // Insert into page
535 const inserted = insertReplyIntoPage(generatedReply);
536
537 if (inserted) {
538 statusDiv.className = 'wb-rg-status success';
539 statusDiv.textContent = '✓ Reply generated and inserted into the page!';
540 } else {
541 statusDiv.className = 'wb-rg-status error';
542 statusDiv.textContent = '⚠ Reply generated but could not insert into page. Copy manually.';
543 }
544
545 } catch (error) {
546 console.error('Error generating reply:', error);
547 statusDiv.className = 'wb-rg-status error';
548 statusDiv.textContent = '✗ Error: ' + error.message;
549 } finally {
550 // Re-enable button
551 generateBtn.disabled = false;
552 generateBtn.textContent = 'Generate Reply';
553 }
554 }
555
556 // ============================================
557 // INITIALIZATION
558 // ============================================
559
560 /**
561 * Initialize the extension
562 */
563 function init() {
564 console.log('Initializing Wildberries Review Reply Generator...');
565
566 // Wait for page to be ready
567 if (document.readyState === 'loading') {
568 document.addEventListener('DOMContentLoaded', createUI);
569 } else {
570 // DOM is already ready
571 createUI();
572 }
573 }
574
575 // Start the extension
576 init();
577
578})();