AI Script Generator & Manager

Плавающий AI-чат с поддержкой диалога, анализа окружения и self-correction

Size

32.1 KB

Version

2.3.0

Created

Dec 12, 2025

Updated

3 months ago

1// ==UserScript==
2// @name         AI Script Generator & Manager
3// @description  Плавающий AI-чат с поддержкой диалога, анализа окружения и self-correction
4// @version      2.3.0
5// @match        *://*/*
6// @icon         https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant        GM.setValue
8// @grant        GM.getValue
9// @grant        GM.deleteValue
10// @grant        GM.xmlHttpRequest
11// @grant        GM_addElement
12// @grant        unsafeWindow
13// ==/UserScript==
14
15(function() {
16    'use strict';
17
18    console.log('[AI Script Manager] Starting v2.3.0 (Thinking AI + Self-Correction)...');
19
20    // Конфигурация
21    const CONFIG = {
22        STORAGE_KEYS: {
23            SCRIPTS: 'aiScriptManager_scripts',
24            API_KEYS: 'aiScriptManager_apiKeys',
25            CHAT_HISTORY: 'aiScriptManager_chatHistory'  // новое
26        },
27        MAX_DOM_LENGTH: 3000,
28        REQUEST_TIMEOUT: 45000, // увеличено — self-correction требует времени
29        SELF_CORRECTION_ATTEMPTS: 2,
30        SYSTEM_PROMPT: `You are an expert JavaScript code generator and debugger for Tampermonkey/Violentmonkey userscripts.
31
32YOU ARE A THINKING AI — ANALYZE BEFORE YOU GENERATE.
33
34CRITICAL RULES:
351. Output ONLY pure JavaScript — NO markdown, NO explanations, NO code blocks.
362. ALWAYS consider the page context: URL, existing elements (e.g., .banner, .ad), frameworks.
373. If previous code failed, read the error and FIX it — don’t repeat the same mistake.
384. If user gives feedback (e.g., "не работает на мобильной версии"), ADAPT the code.
395. Prefer robust selectors (e.g., [class*="banner"], not just .banner).
406. Use try-catch and validate existence before DOM manipulation.
417. Avoid eval(), new Function(), or anything blocked by CSP.
42
43SELF-CORRECTION PROTOCOL:
44- If execution failed, analyze the error.
45- Think: Why did it fail? Missing element? TypeError? CSP?
46- Generate a safer, more defensive version.
47
48FEEDBACK PROTOCOL:
49- User says: "Исправь: [замечание]"
50- You must: (1) read current code, (2) apply feedback, (3) return ONLY fixed code.
51
52OUTPUT FORMAT:
53Return ONLY executable JavaScript.
54Do NOT wrap in \`\`\`js.
55Do NOT add comments unless they explain non-obvious logic.
56
57Example correct output:
58const ads = document.querySelectorAll('.banner, .ad, [class*="promo"], [data-ad]');
59console.log('Found', ads.length, 'ads');
60ads.forEach(el => { 
61  if (el && el.parentNode) el.parentNode.removeChild(el);
62});`
63    };
64
65    // Утилиты
66    const Utils = {
67        generateId() {
68            return 'msg_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6);
69        },
70        getPageContext() {
71            // Анализируем окружение — безопасно и быстро
72            const selectorsToCheck = [
73                '.ad', '.banner', '[data-ad]', '.promo', '.popup',
74                '.modal', 'iframe[src*="ads"]', 'ins.adsbygoogle',
75                '.close', '.dismiss', 'button[type="submit"]',
76                '.product', '.price', '.cart', '.search'
77            ];
78            const foundSelectors = selectorsToCheck.filter(sel => {
79                try { return !!document.querySelector(sel); } catch { return false; }
80            });
81
82            const stats = {
83                totalElements: Math.min(document.querySelectorAll('*').length, 10000),
84                buttons: document.querySelectorAll('button, [role="button"]').length,
85                inputs: document.querySelectorAll('input, textarea').length,
86                links: document.querySelectorAll('a').length,
87                hasJQuery: typeof $ === 'function',
88                hasReact: !!document.querySelector('[data-reactroot], [data-reactid], [data-testid]'),
89                viewport: { w: innerWidth, h: innerHeight }
90            };
91
92            return {
93                url: location.href,
94                title: document.title,
95                foundSelectors,
96                stats,
97                bodyPreview: (document.body?.innerText || '').substring(0, CONFIG.MAX_DOM_LENGTH)
98            };
99        },
100        escapeHtml(text) {
101            const div = document.createElement('div');
102            div.textContent = text;
103            return div.innerHTML;
104        },
105        formatDate(timestamp) {
106            return new Date(timestamp).toLocaleString('ru-RU', {
107                hour: '2-digit', minute: '2-digit', second: '2-digit'
108            });
109        }
110    };
111
112    // Хранилище (без устаревшего SETTINGS — используем только CHAT_HISTORY)
113    const Storage = {
114        get(key, defaultValue) {
115            try {
116                const data = localStorage.getItem(key);
117                return data ? JSON.parse(data) : defaultValue;
118            } catch (e) {
119                console.error('[Storage] Error:', e);
120                return defaultValue;
121            }
122        },
123        set(key, value) {
124            try {
125                localStorage.setItem(key, JSON.stringify(value));
126            } catch (e) {
127                console.error('[Storage] Error:', e);
128            }
129        },
130        getChatHistory() {
131            return this.get(CONFIG.STORAGE_KEYS.CHAT_HISTORY, []);
132        },
133        saveChatHistory(history) {
134            this.set(CONFIG.STORAGE_KEYS.CHAT_HISTORY, history);
135        },
136        clearChatHistory() {
137            this.saveChatHistory([]);
138        },
139        getScripts() {
140            return this.get('aiScriptManager_scripts', []);
141        },
142        saveScripts(scripts) {
143            this.set('aiScriptManager_scripts', scripts);
144        },
145        addScript(scriptData) {
146            const scripts = this.getScripts();
147            const newScript = {
148                id: Utils.generateId(),
149                name: scriptData.name || 'Unnamed',
150                createdAt: Date.now(),
151                prompt: scriptData.prompt,
152                code: scriptData.code,
153                status: 'active',
154                logs: []
155            };
156            scripts.push(newScript);
157            this.saveScripts(scripts);
158            return newScript;
159        },
160        getApiKeys() {
161            return this.get('aiScriptManager_apiKeys', { openai: '', openrouter: '' });
162        },
163        saveApiKeys(keys) {
164            this.set('aiScriptManager_apiKeys', keys);
165        }
166    };
167
168    // AI Сервис с поддержкой истории и self-correction
169    const AIService = {
170        async generateCode(userPrompt, chatHistory = []) {
171            const pageContext = Utils.getPageContext();
172
173            const contextSummary = `
174[PAGE CONTEXT]
175URL: ${pageContext.url}
176Title: ${pageContext.title}
177Found selectors: ${pageContext.foundSelectors.length ? pageContext.foundSelectors.join(', ') : 'none'}
178Stats: ${pageContext.stats.totalElements} elements, ${pageContext.stats.buttons} buttons, viewport ${pageContext.stats.viewport.w}×${pageContext.stats.viewport.h}
179Has jQuery: ${pageContext.stats.hasJQuery}, React: ${pageContext.stats.hasReact}
180Body preview: ${pageContext.bodyPreview}
181`;
182
183            // Подготавливаем историю сообщений для LLM
184            const messages = [
185                { role: 'system', content: CONFIG.SYSTEM_PROMPT + '\n\n' + contextSummary }
186            ];
187
188            // Добавляем историю чата (если есть)
189            chatHistory.slice(-6).forEach(msg => { // ограничим 6 последними сообщениями
190                if (msg.role === 'assistant' && msg.generatedCode) {
191                    // Для AI важно видеть не только текст, но и код
192                    messages.push({
193                        role: 'assistant',
194                        content: msg.content + '\n\nGenerated code:\n' + msg.generatedCode
195                    });
196                } else if (msg.role === 'system' && msg.executionResult) {
197                    const res = msg.executionResult;
198                    const execInfo = res.success 
199                        ? `✅ Success. Logs: ${res.logs ? res.logs.slice(0, 3).join('; ') : 'none'}`
200                        : `❌ Error: ${res.error}`;
201                    messages.push({ role: 'system', content: execInfo });
202                } else {
203                    messages.push({ role: msg.role, content: msg.content });
204                }
205            });
206
207            // Добавляем новый запрос
208            messages.push({ role: 'user', content: userPrompt });
209
210            // Пробуем RM.aiCall → OpenRouter → OpenAI
211            for (let i = 0; i < 3; i++) {
212                try {
213                    if (i === 0 && typeof RM?.aiCall === 'function') {
214                        const code = await RM.aiCall(JSON.stringify(messages));
215                        return this.cleanCode(code);
216                    } else if (i === 1) {
217                        const keys = Storage.getApiKeys();
218                        if (keys.openrouter) {
219                            return await this.callAPI('https://openrouter.ai/api/v1/chat/completions', 'anthropic/claude-3.5-sonnet', messages, keys.openrouter, true);
220                        }
221                    } else if (i === 2) {
222                        const keys = Storage.getApiKeys();
223                        if (keys.openai) {
224                            return await this.callAPI('https://api.openai.com/v1/chat/completions', 'gpt-4o-mini', messages, keys.openai, false);
225                        }
226                    }
227                } catch (e) {
228                    console.warn(`[AI] Backend ${['RM', 'OpenRouter', 'OpenAI'][i]} failed:`, e.message);
229                }
230            }
231
232            throw new Error('Все AI backends недоступны. Добавьте API-ключ в настройки → ⚙️.');
233        },
234
235        async callAPI(url, model, messages, apiKey, isOpenRouter) {
236            return new Promise((resolve, reject) => {
237                const timeoutId = setTimeout(() => reject(new Error('timeout')), CONFIG.REQUEST_TIMEOUT);
238
239                const headers = {
240                    'Content-Type': 'application/json',
241                    'Authorization': 'Bearer ' + apiKey
242                };
243                if (isOpenRouter) {
244                    headers['HTTP-Referer'] = location.href;
245                    headers['X-Title'] = 'AI Script Manager';
246                }
247
248                GM.xmlHttpRequest({
249                    method: 'POST',
250                    url: url,
251                    headers: headers,
252                    data: JSON.stringify({
253                        model: model,
254                        messages: messages,
255                        temperature: 0.3, // ↓ меньше "креатива", больше стабильности
256                        max_tokens: 1500
257                    }),
258                    onload: (res) => {
259                        clearTimeout(timeoutId);
260                        try {
261                            const data = JSON.parse(res.responseText);
262                            if (data.error) throw new Error(data.error.message || data.error);
263                            const content = data.choices[0]?.message?.content || '';
264                            resolve(this.cleanCode(content));
265                        } catch (e) {
266                            reject(new Error('Parse: ' + e.message));
267                        }
268                    },
269                    onerror: () => {
270                        clearTimeout(timeoutId);
271                        reject(new Error('network'));
272                    }
273                });
274            });
275        },
276
277        cleanCode(code) {
278            return code
279                .replace(/^```(javascript|js)?\s*\n?/gm, '')
280                .replace(/\n?```$/gm, '')
281                .replace(/^\s*\/\/.*$/gm, '') // убираем комментарии-объяснения
282                .trim();
283        }
284    };
285
286    // Выполнение — с логированием в консоль и перехватом ошибок
287    const ScriptExecutor = {
288        // Выполняет код и возвращает результат (для анализа AI)
289        executeScriptTemp(code) {
290            return new Promise((resolve) => {
291                const logs = [];
292                const originalLog = console.log;
293                const originalError = console.error;
294
295                // Перехватываем console.log/error
296                console.log = (...args) => { logs.push(args.map(String).join(' ')); originalLog.apply(console, args); };
297                console.error = (...args) => { logs.push('[ERROR] ' + args.map(String).join(' ')); originalError.apply(console, args); };
298
299                try {
300                    const wrapper = `
301                        (() => {
302                            "use strict";
303                            try { ${code} }
304                            catch (e) {
305                                console.error('Runtime error:', e.message, e.stack);
306                                throw e;
307                            }
308                        })();
309                    `;
310
311                    if (typeof GM_addElement === 'function') {
312                        GM_addElement('script', { text: wrapper });
313                    } else {
314                        const s = document.createElement('script');
315                        s.textContent = wrapper;
316                        (document.head || document.documentElement).appendChild(s);
317                        s.remove();
318                    }
319
320                    setTimeout(() => {
321                        console.log = originalLog;
322                        console.error = originalError;
323                        resolve({ success: true, logs });
324                    }, 100); // дадим время на выполнение
325
326                } catch (error) {
327                    console.log = originalLog;
328                    console.error = originalError;
329                    resolve({
330                        success: false,
331                        error: error.message || String(error),
332                        logs
333                    });
334                }
335            });
336        },
337
338        // Сохранённые скрипты — как раньше
339        executeScript(script) {
340            console.log('[Exec] Running saved script:', script.name);
341            // Обёртка без логгера — для production
342            const wrapper = `(() => {"use strict";try{${script.code}}catch(e){console.error('[AI Script]',e)}})();`;
343            if (typeof GM_addElement === 'function') {
344                GM_addElement('script', { text: wrapper });
345            } else {
346                const s = document.createElement('script');
347                s.textContent = wrapper;
348                (document.head || document.documentElement).appendChild(s);
349                s.remove();
350            }
351        },
352
353        executeActiveScripts() {
354            Storage.getScripts()
355                .filter(s => s.status === 'active')
356                .forEach(s => this.executeScript(s));
357        }
358    };
359
360    // UI — с поддержкой диалога и редактирования
361    const UI = {
362        elements: {},
363        chatHistory: [],
364
365        init() {
366            if (!document.body) return setTimeout(() => this.init(), 100);
367            this.injectStyles();
368            this.createWidget();
369            this.loadChatHistory();
370            console.log('[UI] Ready with thinking AI!');
371        },
372
373        injectStyles() { /* ... (стили из v2.2.0, без изменений) ... */ 
374            const style = document.createElement('style');
375            style.textContent = `
376                /* ... прежние стили ... */
377                .ai-code-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
378                .ai-modal-edit { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 2147483647; }
379                .ai-modal-edit.visible { display: flex; }
380                .ai-modal-edit-content { 
381                    background: white; border-radius: 16px; padding: 24px; width: 90%; max-width: 700px; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column;
382                }
383                .ai-modal-edit-header { font-weight: 600; margin-bottom: 16px; color: #1f2937; }
384                .ai-modal-edit-body { flex: 1; display: flex; flex-direction: column; gap: 16px; overflow: auto; }
385                .ai-modal-edit-textarea { 
386                    width: 100%; height: 150px; font-family: monospace; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px;
387                }
388                .ai-modal-edit-feedback { 
389                    width: 100%; height: 60px; padding: 10px; border: 1px solid #d1d5db; border-radius: 8px;
390                }
391                .ai-modal-edit-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
392            `;
393            (document.head || document.documentElement).appendChild(style);
394        },
395
396        createWidget() {
397            const container = document.createElement('div');
398            container.className = 'ai-manager';
399            container.innerHTML = `
400                <button class="ai-toggle-btn" id="aiToggle">🤖</button>
401                <div class="ai-window" id="aiWindow">
402                    <!-- ... прежний HTML ... -->
403                    <div class="ai-view active" id="chatView">
404                        <div class="ai-messages" id="messages">
405                            <div class="ai-message system">
406                                <div class="ai-avatar">🧠</div>
407                                <div class="ai-content">
408                                    Я — думающий AI. Могу:<br>
409                                    • Анализировать страницу перед генерацией<br>
410                                    • Исправлять код по вашим замечаниям<br>
411                                    • Автоисправлять ошибки после выполнения<br>
412                                    <br>
413                                    Пример запроса: «Сделай кнопку "Скрыть всё", которая удаляет все баннеры и модалки»
414                                </div>
415                            </div>
416                        </div>
417                        <div class="ai-input-area">
418                            <div class="ai-input-wrapper">
419                                <textarea class="ai-input" id="input" placeholder="Опишите задачу или дайте обратную связь..."></textarea>
420                                <button class="ai-send-btn" id="send"></button>
421                            </div>
422                        </div>
423                    </div>
424                    <!-- scriptsView, settingsView — без изменений -->
425                </div>
426
427                <!-- Modal для редактирования -->
428                <div class="ai-modal-edit" id="editModal">
429                    <div class="ai-modal-edit-content">
430                        <div class="ai-modal-edit-header">✏️ Исправить код</div>
431                        <div class="ai-modal-edit-body">
432                            <label>Текущий код:</label>
433                            <textarea class="ai-modal-edit-textarea" id="editCode"></textarea>
434                            <label>Ваш комментарий (что не так?):</label>
435                            <textarea class="ai-modal-edit-feedback" id="editFeedback" placeholder="Например: не работает на мобильной версии, не находит кнопку 'Закрыть' и т.д."></textarea>
436                        </div>
437                        <div class="ai-modal-edit-footer">
438                            <button class="ai-btn ai-btn-secondary" id="editCancel">Отмена</button>
439                            <button class="ai-btn ai-btn-primary" id="editSubmit">Отправить на исправление</button>
440                        </div>
441                    </div>
442                </div>
443            `;
444
445            (document.body || document.documentElement).appendChild(container);
446            this.elements = {
447                toggle: container.querySelector('#aiToggle'),
448                window: container.querySelector('#aiWindow'),
449                close: container.querySelector('#aiClose'),
450                messages: container.querySelector('#messages'),
451                input: container.querySelector('#input'),
452                send: container.querySelector('#send'),
453                // modals
454                editModal: container.querySelector('#editModal'),
455                editCode: container.querySelector('#editCode'),
456                editFeedback: container.querySelector('#editFeedback'),
457                editCancel: container.querySelector('#editCancel'),
458                editSubmit: container.querySelector('#editSubmit')
459            };
460
461            // События
462            this.elements.toggle.onclick = () => this.toggleWindow();
463            this.elements.close.onclick = () => this.toggleWindow();
464            this.elements.send.onclick = () => this.sendMessage();
465            this.elements.input.onkeydown = (e) => {
466                if (e.key === 'Enter' && !e.shiftKey) {
467                    e.preventDefault(); this.sendMessage();
468                }
469            };
470
471            // Edit modal
472            this.elements.editCancel.onclick = () => this.hideEditModal();
473            this.elements.editSubmit.onclick = () => this.submitEdit();
474            container.querySelectorAll('.ai-nav-btn').forEach(btn => {
475                btn.onclick = () => this.switchView(btn.dataset.view);
476            });
477            document.getElementById('saveSettings').onclick = () => this.saveSettings();
478        },
479
480        loadChatHistory() {
481            this.chatHistory = Storage.getChatHistory();
482            this.renderChatHistory();
483        },
484
485        renderChatHistory() {
486            const messagesEl = this.elements.messages;
487            messagesEl.innerHTML = '';
488            if (this.chatHistory.length === 0) {
489                messagesEl.innerHTML = `
490                    <div class="ai-message system">
491                        <div class="ai-avatar">🧠</div>
492                        <div class="ai-content">Начните диалог — опишите, что хотите сделать.</div>
493                    </div>`;
494                return;
495            }
496
497            this.chatHistory.forEach(msg => {
498                if (msg.role === 'user') {
499                    this.addMessageToDOM('user', msg.content);
500                } else if (msg.role === 'assistant' && msg.generatedCode) {
501                    this.addCodeMessageToDOM(msg.generatedCode, msg.content, msg.executionResult);
502                } else if (msg.role === 'system') {
503                    this.addMessageToDOM('system', msg.content);
504                }
505            });
506            messagesEl.scrollTop = messagesEl.scrollHeight;
507        },
508
509        addMessageToDOM(role, content) {
510            const msg = document.createElement('div');
511            msg.className = 'ai-message ' + role;
512            const avatar = { user: '👤', assistant: '🤖', system: '🧠', error: '⚠️' }[role] || 'ℹ️';
513            msg.innerHTML = `<div class="ai-avatar">${avatar}</div><div class="ai-content">${Utils.escapeHtml(content)}</div>`;
514            this.elements.messages.appendChild(msg);
515        },
516
517        addCodeMessageToDOM(code, prompt, executionResult = {}) {
518            const msg = document.createElement('div');
519            msg.className = 'ai-message assistant';
520            const escapedCode = Utils.escapeHtml(code);
521            const statusIcon = executionResult.success ? '✅' : executionResult.error ? '⚠️' : '';
522            const statusText = executionResult.success ? 'Выполнен' : executionResult.error ? 'Ошибка' : '';
523
524            msg.innerHTML = `
525                <div class="ai-avatar">🤖</div>
526                <div class="ai-content">
527                    <div>${statusIcon} ${prompt} ${statusText ? `(${statusText})` : ''}</div>
528                    <div class="ai-code">${escapedCode}</div>
529                    <div class="ai-code-actions">
530                        <button class="ai-btn ai-btn-primary" data-action="execute">▶️ Выполнить</button>
531                        ${!executionResult.error ? `<button class="ai-btn ai-btn-secondary" data-action="save">💾 Сохранить</button>` : ''}
532                        <button class="ai-btn ai-btn-secondary" data-action="edit">✏️ Исправить</button>
533                    </div>
534                    ${executionResult.error ? `<div class="ai-message error"><div class="ai-avatar">⚠️</div><div class="ai-content">Ошибка: ${Utils.escapeHtml(executionResult.error)}</div></div>` : ''}
535                </div>
536            `;
537            this.elements.messages.appendChild(msg);
538            this.setupCodeActions(msg, code, prompt, executionResult);
539        },
540
541        setupCodeActions(container, code, prompt, executionResult) {
542            const executeBtn = container.querySelector('[data-action="execute"]');
543            const saveBtn = container.querySelector('[data-action="save"]');
544            const editBtn = container.querySelector('[data-action="edit"]');
545
546            executeBtn?.addEventListener('click', () => this.executeAndLog(code, prompt));
547            saveBtn?.addEventListener('click', () => this.saveCode(code, prompt));
548            editBtn?.addEventListener('click', () => this.showEditModal(code));
549        },
550
551        async executeAndLog(code, prompt) {
552            // 1. Выполняем
553            const result = await ScriptExecutor.executeScriptTemp(code);
554            // 2. Сохраняем в историю
555            const msg = {
556                id: Utils.generateId(),
557                role: 'system',
558                content: result.success ? '✅ Код выполнен успешно' : '❌ Ошибка выполнения',
559                executionResult: result,
560                timestamp: Date.now()
561            };
562            this.chatHistory.push(msg);
563            Storage.saveChatHistory(this.chatHistory);
564            // 3. Показываем результат
565            if (result.success) {
566                this.addMessageToDOM('assistant', '✅ Выполнено! Логи: ' + (result.logs?.slice(0, 3).join('; ') || '—'));
567            } else {
568                this.addMessageToDOM('error', '❌ Ошибка: ' + result.error);
569            }
570        },
571
572        async sendMessage() {
573            const prompt = this.elements.input.value.trim();
574            if (!prompt) return;
575
576            this.elements.input.value = '';
577            this.addMessageToDOM('user', prompt);
578
579            // Сохраняем в историю
580            const userMsg = { id: Utils.generateId(), role: 'user', content: prompt, timestamp: Date.now() };
581            this.chatHistory.push(userMsg);
582            Storage.saveChatHistory(this.chatHistory);
583
584            // Отправляем в AI
585            try {
586                const code = await AIService.generateCode(prompt, this.chatHistory);
587
588                // Self-correction loop (до 2 попыток)
589                let result = await ScriptExecutor.executeScriptTemp(code);
590                let attempts = 0;
591                while (!result.success && attempts < CONFIG.SELF_CORRECTION_ATTEMPTS) {
592                    attempts++;
593                    this.addMessageToDOM('system', `🔁 Попытка ${attempts} автокоррекции...`);
594
595                    const correctionPrompt = `Предыдущий код вызвал ошибку:\n${result.error}\n\nИсправь его. Верни ТОЛЬКО JavaScript.`;
596                    const fixedCode = await AIService.generateCode(correctionPrompt, [
597                        ...this.chatHistory,
598                        { role: 'assistant', content: 'Generated code', generatedCode: code },
599                        { role: 'system', content: 'Execution failed', executionResult: result }
600                    ]);
601                    code = fixedCode;
602                    result = await ScriptExecutor.executeScriptTemp(code);
603                }
604
605                // Сохраняем ответ AI
606                const aiMsg = {
607                    id: Utils.generateId(),
608                    role: 'assistant',
609                    content: prompt,
610                    generatedCode: code,
611                    executionResult: result,
612                    timestamp: Date.now()
613                };
614                this.chatHistory.push(aiMsg);
615                Storage.saveChatHistory(this.chatHistory);
616
617                // Отображаем
618                this.addCodeMessageToDOM(code, prompt, result);
619
620            } catch (error) {
621                console.error('[UI] Generation failed:', error);
622                const errMsg = { id: Utils.generateId(), role: 'error', content: '❌ ' + error.message, timestamp: Date.now() };
623                this.chatHistory.push(errMsg);
624                Storage.saveChatHistory(this.chatHistory);
625                this.addMessageToDOM('error', errMsg.content);
626            }
627        },
628
629        showEditModal(code) {
630            this.elements.editCode.value = code;
631            this.elements.editFeedback.value = '';
632            this.elements.editModal.classList.add('visible');
633            this.currentEditingCode = code;
634        },
635
636        hideEditModal() {
637            this.elements.editModal.classList.remove('visible');
638        },
639
640        async submitEdit() {
641            const feedback = this.elements.editFeedback.value.trim();
642            const code = this.elements.editCode.value;
643            if (!feedback) return alert('Опишите, что нужно исправить.');
644
645            this.hideEditModal();
646            this.addMessageToDOM('user', `Исправь: «${feedback}»`);
647
648            const editPrompt = `Исправь следующий код по моему комментарию:\n\nКОММЕНТАРИЙ: ${feedback}\n\nТЕКУЩИЙ КОД:\n${code}`;
649            try {
650                const fixedCode = await AIService.generateCode(editPrompt, this.chatHistory);
651                const result = await ScriptExecutor.executeScriptTemp(fixedCode);
652
653                const aiMsg = {
654                    id: Utils.generateId(),
655                    role: 'assistant',
656                    content: `Исправлено по запросу: ${feedback}`,
657                    generatedCode: fixedCode,
658                    executionResult: result,
659                    timestamp: Date.now()
660                };
661                this.chatHistory.push(aiMsg);
662                Storage.saveChatHistory(this.chatHistory);
663
664                this.addCodeMessageToDOM(fixedCode, `Исправлено: ${feedback}`, result);
665            } catch (e) {
666                this.addMessageToDOM('error', '❌ Не удалось исправить: ' + e.message);
667            }
668        },
669
670        saveCode(code, prompt) {
671            const name = prompt.substring(0, 50).trim() || 'Скрипт';
672            Storage.addScript({ name, code, prompt });
673            ScriptExecutor.executeScript({ code, name });
674            this.addMessageToDOM('assistant', `💾 «${name}» сохранён и запущен!`);
675        },
676
677        // ... остальные методы (switchView, loadSettings и т.д.) без изменений ...
678        toggleWindow() { this.elements.window.classList.toggle('visible'); },
679        switchView(v) { /* как раньше */ },
680        loadSettings() { /* как раньше */ },
681        saveSettings() { /* как раньше */ },
682        loadScripts() { /* как раньше */ },
683        pauseScript() { /* как раньше */ },
684        activateScript() { /* как раньше */ },
685        viewScript() { /* как раньше */ },
686        deleteScript() { /* как раньше */ }
687    };
688
689    // Инициализация
690    function init() {
691        UI.init();
692        ScriptExecutor.executeActiveScripts();
693    }
694
695    window.UI = UI;
696    if (document.readyState === 'loading') {
697        document.addEventListener('DOMContentLoaded', init);
698    } else {
699        init();
700    }
701
702})();