您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Modern UI + Context Menu AI Features (Summarize, Rephrase, Grammar) for forum.blackrussia.online using Google Gemini
// ==UserScript== // @name Black Russia Forum Enhanced AI Helper (v0.6) // @namespace http://tampermonkey.net/ // @version 0.8 // @description Modern UI + Context Menu AI Features (Summarize, Rephrase, Grammar) for forum.blackrussia.online using Google Gemini // @author M. Ageev (Gemini AI) // @match https://forum.blackrussia.online/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect generativelanguage.googleapis.com // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration for Google Gemini API --- const API_ENDPOINT_BASE = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'; // Using Flash for speed/cost const API_METHOD = 'POST'; const STORAGE_KEY = 'br_gemini_api_key_v2'; // Use a new key for potentially different structure needs // --- State Variables --- let apiKey = GM_getValue(STORAGE_KEY, ''); let apiStatus = 'Not Configured'; let settingsPanelVisible = false; let contextMenuVisible = false; let resultModalVisible = false; let currentSelection = null; // Store currently selected text info // --- DOM Elements (created later) --- let settingsPanel, toggleIcon, contextMenu, resultModal, resultModalContent, resultModalCopyBtn, resultModalCloseBtn; // --- Helper Functions (API Interaction - slightly modified) --- function constructApiRequestData(prompt, task = 'generate') { // Customize safety/generation settings based on task if needed let generationConfig = { temperature: 0.7, // Balanced default maxOutputTokens: 2048, }; if (task === 'summarize') generationConfig.temperature = 0.5; if (task === 'rephrase') generationConfig.temperature = 0.8; if (task === 'grammar') generationConfig.temperature = 0.3; // More deterministic for grammar return JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: generationConfig, safetySettings: [ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_MEDIUM_AND_ABOVE" } ] }); } function parseApiResponse(responseDetails) { // (Same parsing logic as before - checking candidates, promptFeedback, errors) try { const response = JSON.parse(responseDetails.responseText); if (response.candidates && response.candidates.length > 0 && response.candidates[0].content?.parts?.[0]?.text) { return response.candidates[0].content.parts[0].text.trim(); } else if (response.promptFeedback?.blockReason) { const reason = response.promptFeedback.blockReason; console.warn('Gemini AI Helper: Prompt blocked due to:', reason, response.promptFeedback.safetyRatings); let blockMessage = `(AI-ответ заблокирован: ${reason})`; const harmfulRating = response.promptFeedback.safetyRatings?.find(r => r.probability !== 'NEGLIGIBLE' && r.probability !== 'LOW'); if(harmfulRating) blockMessage += ` - Причина: ${harmfulRating.category.replace('HARM_CATEGORY_', '')}`; return blockMessage; } else if (response.candidates?.[0]?.finishReason === 'SAFETY') { console.warn('Gemini AI Helper: Response content blocked due to safety settings.', response.candidates[0].safetyRatings); return `(AI-ответ заблокирован из-за настроек безопасности)`; } else { console.error('Gemini AI Helper: Unexpected Gemini API response format:', response); // Try to extract Google's error message if available if (response.error && response.error.message) { return `(Ошибка API: ${response.error.message})`; } return null; } } catch (error) { console.error('Gemini AI Helper: Error parsing Gemini API response:', error, responseDetails.responseText); try { const errResponse = JSON.parse(responseDetails.responseText); if (errResponse.error && errResponse.error.message) return `(Ошибка API: ${errResponse.error.message})`; } catch (e) { /* Ignore inner parse error */ } return null; } } function updateStatusDisplay(newStatus, message = '') { apiStatus = newStatus; const statusEl = document.getElementById('br-ai-status'); const messageEl = document.getElementById('br-ai-status-message'); if (statusEl) { let icon = '❓'; // Default: Unknown if (newStatus === 'Working') icon = '✅'; // Green check else if (newStatus === 'Error' || newStatus === 'Not Configured') icon = '❌'; // Red cross else if (newStatus === 'Checking' || newStatus === 'Saving...') icon = '⏳'; // Hourglass statusEl.innerHTML = `${icon} Статус: ${apiStatus}`; // Use innerHTML for icon statusEl.className = `status-${apiStatus.toLowerCase().replace(/[^a-z0-9-]/g, '-')}`; } if (messageEl) { messageEl.textContent = message; messageEl.style.display = message ? 'block' : 'none'; } } function saveApiKey() { const inputEl = document.getElementById('br-ai-api-key-input'); if (inputEl) { const newApiKey = inputEl.value.trim(); // Basic validation removed for brevity, add back if desired updateStatusDisplay('Saving...'); GM_setValue(STORAGE_KEY, newApiKey); apiKey = newApiKey; console.log('Gemini AI Helper: API Key saved.'); checkApiKey(); // Check the new key } } function checkApiKey(silent = false) { // Add silent option to avoid spamming alerts return new Promise((resolve) => { // Return a promise to know when check is done if (!apiKey) { updateStatusDisplay('Not Configured', 'API ключ не введен.'); return resolve(false); } if (!API_ENDPOINT_BASE) { updateStatusDisplay('Error', 'API Endpoint не настроен в скрипте!'); return resolve(false); } updateStatusDisplay('Checking...', silent ? '' : 'Отправка тестового запроса...'); const testPrompt = "Привет! Просто ответь 'OK'."; const requestData = constructApiRequestData(testPrompt, 'test'); const fullApiUrl = `${API_ENDPOINT_BASE}?key=${apiKey}`; GM_xmlhttpRequest({ method: API_METHOD, url: fullApiUrl, headers: { 'Content-Type': 'application/json' }, data: requestData, timeout: 15000, onload: function(response) { let success = false; if (response.status === 200) { const testResult = parseApiResponse(response); if (testResult !== null && !testResult.startsWith('(')) { // Check it's not an error/blocked message updateStatusDisplay('Working', 'API ключ работает.'); success = true; } else if (testResult !== null) { updateStatusDisplay('Error', `Ключ принят, но ошибка: ${testResult}`); } else { updateStatusDisplay('Error', 'Ключ работает, но не удалось обработать ответ API.'); } } else { let errorMsg = `Ошибка API (Статус: ${response.status}). Проверьте ключ.`; try { const errResp = JSON.parse(response.responseText); if(errResp.error?.message) errorMsg = `Ошибка API: ${errResp.error.message}`; } catch(e){} updateStatusDisplay('Error', errorMsg); console.error('Gemini AI Helper: Key check fail - Status:', response.status, response.responseText); } resolve(success); }, onerror: function(response) { updateStatusDisplay('Error', 'Ошибка сети или CORS. Проверьте @connect.'); console.error('Gemini AI Helper: Key check fail - Network error:', response); resolve(false); }, ontimeout: function() { updateStatusDisplay('Error', 'Тайм-аут запроса к API.'); console.error('Gemini AI Helper: Key check fail - Timeout.'); resolve(false); } }); }); } // --- Core AI Call Function (Modified for Loading Indicator) --- function callAI(prompt, task = 'generate', loadingIndicatorElement = null) { return new Promise(async (resolve) => { // Return promise if (apiStatus !== 'Working') { // Maybe try a silent check? const keyOk = await checkApiKey(true); // Silent check if (!keyOk) { alert('Gemini AI Helper: API не настроен или не работает. Проверьте настройки.'); if (loadingIndicatorElement) loadingIndicatorElement.disabled = false; return resolve(null); // Resolve with null on failure } // If check passed, status is now 'Working', continue } if (!prompt) { alert('Gemini AI Helper: Нет текста для отправки AI.'); if (loadingIndicatorElement) loadingIndicatorElement.disabled = false; return resolve(null); } console.log(`Gemini AI Helper: Calling Gemini for task '${task}'`); if (loadingIndicatorElement) loadingIndicatorElement.disabled = true; // Disable button/element const requestData = constructApiRequestData(prompt, task); const fullApiUrl = `${API_ENDPOINT_BASE}?key=${apiKey}`; GM_xmlhttpRequest({ method: API_METHOD, url: fullApiUrl, headers: { 'Content-Type': 'application/json' }, data: requestData, timeout: 90000, // Increased timeout for potentially longer tasks onload: function(response) { let result = null; if (response.status === 200) { result = parseApiResponse(response); if (result === null) { // Explicit parse error alert('Gemini AI Helper: Ошибка обработки ответа от AI.'); } } else { let errorMsg = `Ошибка API (Статус: ${response.status}).`; try { const errResp = JSON.parse(response.responseText); if(errResp.error?.message) errorMsg = `Ошибка API: ${errResp.error.message}`; } catch(e){} alert(`Gemini AI Helper: ${errorMsg}`); console.error('Gemini AI Helper: AI call fail - Status:', response.status, response.responseText); result = `(Сетевая ошибка: ${response.status})`; // Return error info } if (loadingIndicatorElement) loadingIndicatorElement.disabled = false; resolve(result); // Resolve promise with result (string or null) }, onerror: function(response) { alert('Gemini AI Helper: Ошибка сети при вызове AI.'); console.error('Gemini AI Helper: AI call fail - Network error:', response); if (loadingIndicatorElement) loadingIndicatorElement.disabled = false; resolve('(Сетевая ошибка)'); }, ontimeout: function() { alert('Gemini AI Helper: Тайм-аут при вызове AI.'); console.error('Gemini AI Helper: AI call fail - Timeout.'); if (loadingIndicatorElement) loadingIndicatorElement.disabled = false; resolve('(Тайм-аут)'); }, }); }); } // --- UI Creation --- /** Inject CSS */ function injectStyles() { GM_addStyle(` /* --- CSS Variables (Theme) --- */ :root { --ai-bg-primary: #2d2d2d; --ai-bg-secondary: #3a3a3a; --ai-bg-tertiary: #454545; --ai-text-primary: #e0e0e0; --ai-text-secondary: #b0b0b0; --ai-accent-primary: #00aaff; /* Bright blue */ --ai-accent-secondary: #00cfaa; /* Teal */ --ai-success: #4CAF50; --ai-error: #F44336; --ai-warning: #FFC107; --ai-info: #2196F3; --ai-border-color: #555555; --ai-border-radius: 6px; --ai-font-family: 'Roboto', 'Segoe UI', sans-serif; /* Modern font stack */ --ai-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); --ai-transition: all 0.25s ease-in-out; } /* --- Toggle Icon --- */ #br-ai-toggle-icon { position: fixed; bottom: 25px; right: 25px; width: 50px; height: 50px; background: linear-gradient(135deg, var(--ai-accent-primary), var(--ai-accent-secondary)); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 26px; cursor: pointer; z-index: 10000; box-shadow: var(--ai-shadow); user-select: none; transition: var(--ai-transition), transform 0.15s ease; border: none; } #br-ai-toggle-icon:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(0, 180, 220, 0.4); } #br-ai-toggle-icon:active { transform: scale(0.95); } /* --- Settings Panel --- */ #br-ai-settings-panel { position: fixed; bottom: 90px; right: 25px; width: 350px; background-color: var(--ai-bg-primary); color: var(--ai-text-primary); border: 1px solid var(--ai-border-color); border-radius: var(--ai-border-radius); padding: 0; /* Remove padding, handle inside */ z-index: 10001; box-shadow: var(--ai-shadow); font-family: var(--ai-font-family); font-size: 14px; overflow: hidden; /* Needed for border-radius with tabs */ transform: translateY(10px) scale(0.98); opacity: 0; /* Initial state for transition */ transition: var(--ai-transition), transform 0.2s ease, opacity 0.2s ease; display: none; /* Controlled by JS */ } #br-ai-settings-panel.visible { display: block; transform: translateY(0) scale(1); opacity: 1; } /* Panel Tabs */ .br-ai-panel-tabs { display: flex; background-color: var(--ai-bg-secondary); border-bottom: 1px solid var(--ai-border-color); } .br-ai-panel-tab { flex: 1; padding: 12px 15px; text-align: center; cursor: pointer; color: var(--ai-text-secondary); border-bottom: 3px solid transparent; transition: var(--ai-transition); font-weight: 500; } .br-ai-panel-tab:hover { background-color: var(--ai-bg-tertiary); color: var(--ai-text-primary); } .br-ai-panel-tab.active { color: var(--ai-text-primary); border-bottom-color: var(--ai-accent-primary); } /* Panel Content Area */ .br-ai-panel-content { padding: 20px; } .br-ai-tab-pane { display: none; } .br-ai-tab-pane.active { display: block; } /* Form Elements */ #br-ai-settings-panel h3 { margin-top: 0; margin-bottom: 15px; font-size: 18px; font-weight: 500; color: var(--ai-text-primary); padding-bottom: 8px; border-bottom: 1px solid var(--ai-border-color); } #br-ai-settings-panel label { display: block; margin-bottom: 6px; font-weight: 500; color: var(--ai-text-secondary); } #br-ai-api-key-input { width: 100%; padding: 10px 12px; margin-bottom: 15px; background-color: var(--ai-bg-tertiary); color: var(--ai-text-primary); border: 1px solid var(--ai-border-color); border-radius: var(--ai-border-radius); font-size: 14px; box-sizing: border-box; /* Include padding in width */ transition: var(--ai-transition); } #br-ai-api-key-input:focus { border-color: var(--ai-accent-primary); box-shadow: 0 0 0 2px rgba(0, 170, 255, 0.3); outline: none; } .br-ai-button-group { display: flex; gap: 10px; margin-top: 10px; } #br-ai-settings-panel button { flex-grow: 1; /* Make buttons fill space */ padding: 10px 15px; font-size: 14px; font-weight: 500; border: none; border-radius: var(--ai-border-radius); cursor: pointer; transition: var(--ai-transition); display: flex; align-items: center; justify-content: center; gap: 5px; } #br-ai-save-key { background-color: var(--ai-success); color: white; } #br-ai-test-key { background-color: var(--ai-info); color: white; } #br-ai-settings-panel button:hover { filter: brightness(1.1); } #br-ai-settings-panel button:active { filter: brightness(0.9); transform: scale(0.98); } #br-ai-settings-panel button:disabled { background-color: var(--ai-bg-tertiary); color: var(--ai-text-secondary); cursor: not-allowed; } /* Status Display */ #br-ai-status { margin-top: 20px; font-weight: 500; display: flex; align-items: center; gap: 8px; } #br-ai-status-message { margin-top: 8px; font-size: 13px; color: var(--ai-text-secondary); word-wrap: break-word; line-height: 1.4; } /* Status colors */ .status-not-configured, .status-error { color: var(--ai-error); } .status-checking, .status-saving { color: var(--ai-warning); } .status-working { color: var(--ai-success); } /* About Tab */ #br-ai-about p { margin-bottom: 10px; line-height: 1.5; color: var(--ai-text-secondary); } #br-ai-about strong { color: var(--ai-text-primary); } #br-ai-about a { color: var(--ai-accent-primary); text-decoration: none; } #br-ai-about a:hover { text-decoration: underline; } #br-ai-about ul { list-style: disc; padding-left: 25px; margin-top: 10px;} #br-ai-about li { margin-bottom: 5px;} /* --- Context Menu --- */ #br-ai-context-menu { position: absolute; /* Positioned by JS */ min-width: 180px; background-color: var(--ai-bg-secondary); border: 1px solid var(--ai-border-color); border-radius: var(--ai-border-radius); box-shadow: var(--ai-shadow); z-index: 10005; /* Above panel */ padding: 5px 0; font-family: var(--ai-font-family); font-size: 14px; opacity: 0; /* Start hidden */ transform: scale(0.95); transition: opacity 0.15s ease, transform 0.15s ease; display: none; /* Controlled by JS */ } #br-ai-context-menu.visible { display: block; opacity: 1; transform: scale(1); } .br-ai-context-menu-item { display: flex; /* Use flex for icon alignment */ align-items: center; gap: 8px; padding: 8px 15px; color: var(--ai-text-primary); cursor: pointer; transition: background-color 0.15s ease; } .br-ai-context-menu-item:hover { background-color: var(--ai-bg-tertiary); } .br-ai-context-menu-item span { flex-grow: 1; } /* Text takes remaining space */ .br-ai-context-menu-item i { /* Basic icon styling */ font-style: normal; width: 1.2em; /* Ensure consistent icon space */ text-align: center; color: var(--ai-accent-secondary); } /* --- Result Modal --- */ #br-ai-result-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.95); width: 90%; max-width: 600px; background-color: var(--ai-bg-primary); color: var(--ai-text-primary); border: 1px solid var(--ai-border-color); border-radius: var(--ai-border-radius); box-shadow: var(--ai-shadow); z-index: 10010; /* Above context menu */ font-family: var(--ai-font-family); opacity: 0; transition: opacity 0.25s ease, transform 0.25s ease; display: none; /* Controlled by JS */ } #br-ai-result-modal.visible { display: block; opacity: 1; transform: translate(-50%, -50%) scale(1); } .br-ai-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background-color: var(--ai-bg-secondary); border-bottom: 1px solid var(--ai-border-color); border-top-left-radius: var(--ai-border-radius); /* Match parent */ border-top-right-radius: var(--ai-border-radius); } .br-ai-modal-header h4 { margin: 0; font-size: 16px; font-weight: 500; } #br-ai-result-modal-close { background: none; border: none; color: var(--ai-text-secondary); font-size: 20px; cursor: pointer; padding: 0 5px; transition: color 0.15s ease; } #br-ai-result-modal-close:hover { color: var(--ai-text-primary); } .br-ai-modal-content { padding: 15px; max-height: 60vh; /* Limit height and allow scrolling */ overflow-y: auto; font-size: 14px; line-height: 1.6; white-space: pre-wrap; /* Preserve whitespace and newlines */ background-color: var(--ai-bg-tertiary); /* Slightly different bg for content */ margin: 15px; /* Add margin around content */ border-radius: 4px; } /* Style scrollbar for modal content */ .br-ai-modal-content::-webkit-scrollbar { width: 8px; } .br-ai-modal-content::-webkit-scrollbar-track { background: var(--ai-bg-secondary); border-radius: 4px; } .br-ai-modal-content::-webkit-scrollbar-thumb { background: var(--ai-border-color); border-radius: 4px; } .br-ai-modal-content::-webkit-scrollbar-thumb:hover { background: #6b6b6b; } .br-ai-modal-footer { padding: 10px 15px; text-align: right; border-top: 1px solid var(--ai-border-color); } #br-ai-result-modal-copy { padding: 8px 15px; background-color: var(--ai-accent-primary); color: white; border: none; border-radius: var(--ai-border-radius); cursor: pointer; font-size: 14px; font-weight: 500; transition: var(--ai-transition); } #br-ai-result-modal-copy:hover { filter: brightness(1.1); } #br-ai-result-modal-copy:active { filter: brightness(0.9); } /* Editor Button Styling (from previous version, maybe adjusted) */ .br-ai-editor-button { margin-left: 8px; padding: 5px 10px !important; font-size: 13px !important; line-height: 1.5 !important; cursor: pointer; background-color: var(--ai-accent-secondary); /* Teal */ color: white; border: none; border-radius: 4px; transition: background-color 0.2s ease; display: inline-flex; align-items: center; gap: 4px; /* Align icon/text */ } .br-ai-editor-button:hover { filter: brightness(1.1); } .br-ai-editor-button:disabled { background-color: #aaa; cursor: not-allowed; filter: grayscale(50%); } `); } /** Creates the main settings panel with tabs */ function createSettingsPanel() { // --- Toggle Icon --- toggleIcon = document.createElement('div'); toggleIcon.id = 'br-ai-toggle-icon'; toggleIcon.innerHTML = '✨'; // Sparkles icon toggleIcon.title = 'Настройки Gemini AI Помощника'; document.body.appendChild(toggleIcon); // --- Settings Panel --- settingsPanel = document.createElement('div'); settingsPanel.id = 'br-ai-settings-panel'; settingsPanel.innerHTML = ` <div class="br-ai-panel-tabs"> <div class="br-ai-panel-tab active" data-tab="settings">Настройки</div> <div class="br-ai-panel-tab" data-tab="about">О скрипте</div> </div> <div class="br-ai-panel-content"> <div class="br-ai-tab-pane active" id="br-ai-settings"> <h3>Настройки Gemini AI</h3> <p style="font-size:11px; color:var(--ai-warning); margin-bottom:10px;"><b>Важно:</b> Никогда не делитесь API ключом! <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color:var(--ai-accent-primary);">Управлять ключами</a></p> <label for="br-ai-api-key-input">Ваш Google AI API Ключ:</label> <input type="password" id="br-ai-api-key-input" placeholder="Введите ваш API ключ (начинается с AIza...)"> <div class="br-ai-button-group"> <button id="br-ai-save-key" title="Сохранить ключ и проверить">💾 Сохранить</button> <button id="br-ai-test-key" title="Проверить текущий сохраненный ключ">📡 Проверить</button> </div> <div id="br-ai-status">Статус: Инициализация...</div> <div id="br-ai-status-message"></div> </div> <div class="br-ai-tab-pane" id="br-ai-about"> <h3>О скрипте Enhanced AI Helper</h3> <p><strong>Версия:</strong> 0.6</p> <p>Этот скрипт добавляет функции Google Gemini AI на форум Black Russia:</p> <ul> <li>Генерация текста в редакторе (кнопка ✨ AI).</li> <li>Контекстное меню (при выделении текста): <ul> <li>📝 Суммаризировать</li> <li>🔄 Перефразировать</li> <li>✅ Исправить грамматику</li> </ul> </li> <li>Современная панель настроек API ключа.</li> </ul> <p>Создано с помощью AI и адаптировано.</p> <p><strong>Внимание:</strong> Использование API может быть платным. Ответственность за использование ключа лежит на вас.</p> </div> </div> `; document.body.appendChild(settingsPanel); // --- Event Listeners --- toggleIcon.addEventListener('click', toggleSettingsPanel); // Tab switching logic settingsPanel.querySelectorAll('.br-ai-panel-tab').forEach(tab => { tab.addEventListener('click', () => { settingsPanel.querySelector('.br-ai-panel-tab.active').classList.remove('active'); settingsPanel.querySelector('.br-ai-tab-pane.active').classList.remove('active'); tab.classList.add('active'); settingsPanel.querySelector(`#br-ai-${tab.dataset.tab}`).classList.add('active'); }); }); // Button listeners document.getElementById('br-ai-save-key').addEventListener('click', saveApiKey); document.getElementById('br-ai-test-key').addEventListener('click', () => checkApiKey()); // Non-silent check on button press // Load saved key document.getElementById('br-ai-api-key-input').value = apiKey; } /** Toggles the settings panel visibility */ function toggleSettingsPanel() { settingsPanelVisible = !settingsPanelVisible; if (settingsPanelVisible) { settingsPanel.classList.add('visible'); // Refresh status on open updateStatusDisplay(apiStatus, document.getElementById('br-ai-status-message')?.textContent || ''); } else { settingsPanel.classList.remove('visible'); } } /** Creates the custom context menu (initially hidden) */ function createContextMenu() { contextMenu = document.createElement('div'); contextMenu.id = 'br-ai-context-menu'; contextMenu.innerHTML = ` <div class="br-ai-context-menu-item" data-action="summarize"><i>📝</i> <span>Суммаризировать</span></div> <div class="br-ai-context-menu-item" data-action="rephrase"><i>🔄</i> <span>Перефразировать</span></div> <div class="br-ai-context-menu-item" data-action="grammar"><i>✅</i> <span>Исправить грамматику</span></div> `; document.body.appendChild(contextMenu); // Add listener for menu item clicks contextMenu.addEventListener('click', handleContextMenuAction); } /** Creates the result modal (initially hidden) */ function createResultModal() { resultModal = document.createElement('div'); resultModal.id = 'br-ai-result-modal'; resultModal.innerHTML = ` <div class="br-ai-modal-header"> <h4>Результат AI</h4> <button id="br-ai-result-modal-close" title="Закрыть">×</button> </div> <div class="br-ai-modal-content" id="br-ai-result-modal-content"> Загрузка... </div> <div class="br-ai-modal-footer"> <button id="br-ai-result-modal-copy" title="Копировать результат">📋 Копировать</button> </div> `; document.body.appendChild(resultModal); resultModalContent = document.getElementById('br-ai-result-modal-content'); resultModalCopyBtn = document.getElementById('br-ai-result-modal-copy'); resultModalCloseBtn = document.getElementById('br-ai-result-modal-close'); resultModalCloseBtn.addEventListener('click', hideResultModal); resultModalCopyBtn.addEventListener('click', copyModalContent); // Optional: Close modal on clicking outside resultModal.addEventListener('click', (e) => { if (e.target === resultModal) { // Check if click is on the backdrop itself hideResultModal(); } }); } /** Shows the custom context menu */ function showContextMenu(x, y) { // Hide any previous instances immediately hideContextMenu(); hideResultModal(); // Also hide modal if open contextMenu.style.left = `${x}px`; contextMenu.style.top = `${y}px`; contextMenu.classList.add('visible'); contextMenuVisible = true; } /** Hides the custom context menu */ function hideContextMenu() { if (contextMenu) { contextMenu.classList.remove('visible'); } contextMenuVisible = false; } /** Shows the result modal with content */ function showResultModal(content, title = "Результат AI") { hideContextMenu(); // Hide context menu when showing modal if (!resultModal) createResultModal(); // Create if it doesn't exist yet resultModal.querySelector('.br-ai-modal-header h4').textContent = title; resultModalContent.textContent = content || "Не удалось получить результат."; // Handle null content resultModal.classList.add('visible'); resultModalVisible = true; } /** Hides the result modal */ function hideResultModal() { if (resultModal) { resultModal.classList.remove('visible'); } resultModalVisible = false; } /** Copies the content of the result modal to clipboard */ function copyModalContent() { if (resultModalContent) { navigator.clipboard.writeText(resultModalContent.textContent) .then(() => { // Optional: Show feedback like "Copied!" resultModalCopyBtn.textContent = '✅ Скопировано!'; setTimeout(() => { resultModalCopyBtn.textContent = '📋 Копировать'; }, 1500); }) .catch(err => { console.error('Gemini AI Helper: Failed to copy text: ', err); alert('Не удалось скопировать текст.'); }); } } /** Handles clicks on context menu items */ async function handleContextMenuAction(event) { const menuItem = event.target.closest('.br-ai-context-menu-item'); if (!menuItem || !currentSelection?.text) { hideContextMenu(); return; } const action = menuItem.dataset.action; const selectedText = currentSelection.text; let prompt = ''; let taskTitle = ''; // Change button text to indicate loading menuItem.innerHTML = `<i>⏳</i> <span>Обработка...</span>`; hideContextMenu(); // Hide menu immediately after click switch (action) { case 'summarize': prompt = `Создай краткое содержание (summary) следующего текста:\n\n---\n${selectedText}\n---`; taskTitle = "Суммаризация текста"; break; case 'rephrase': prompt = `Перефразируй следующий текст, сохранив основной смысл, но используя другие слова и структуру предложений:\n\n---\n${selectedText}\n---`; taskTitle = "Перефразирование текста"; break; case 'grammar': prompt = `Проверь и исправь грамматику, орфографию и пунктуацию в следующем тексте. Верни только исправленный текст без дополнительных комментариев:\n\n---\n${selectedText}\n---`; taskTitle = "Исправление грамматики"; break; default: // Restore original text if action not found (shouldn't happen) menuItem.innerHTML = { summarize: '<i>📝</i> <span>Суммаризировать</span>', rephrase: '<i>🔄</i> <span>Перефразировать</span>', grammar: '<i>✅</i> <span>Исправить грамматику</span>' }[action] || menuItem.innerHTML; return; } // Show modal in loading state immediately showResultModal("Запрос к AI...", taskTitle); // Call AI (await the promise) const aiResult = await callAI(prompt, action); // Pass action as task // Update modal with the actual result (or error message) if (resultModalVisible) { // Check if modal wasn't closed by user showResultModal(aiResult || "Ошибка: Нет ответа от AI.", taskTitle); } // Note: No need to restore menu item text here as it's hidden now } /** Adds the AI button to the forum's text editor */ function addAiButtonToEditor() { const editorToolbarSelectors = ['.fr-toolbar', '.xf-editor-toolbar', 'div[data-xf-init="editor"] .fr-toolbar']; let toolbar = null; for (const selector of editorToolbarSelectors) { toolbar = document.querySelector(selector); if (toolbar) break; } if (toolbar && !toolbar.querySelector('.br-ai-editor-button')) { const aiButton = document.createElement('button'); aiButton.innerHTML = '✨ AI'; // Can use innerHTML for icon + text aiButton.title = 'Генерация текста с Gemini AI'; aiButton.classList.add('br-ai-editor-button'); aiButton.type = 'button'; aiButton.addEventListener('click', async (e) => { // Make async e.preventDefault(); if (aiButton.disabled) return; const editorContentArea = document.querySelector('.fr-element.fr-view') || document.querySelector('textarea.input.codeEditor'); if (!editorContentArea) { alert('Gemini AI Helper: Не удалось найти поле ввода редактора.'); return; } const currentText = editorContentArea.innerText || editorContentArea.value || ''; const promptText = prompt('Введите ваш запрос для Gemini AI (генерация текста):', currentText.slice(-500)); if (promptText) { const originalButtonText = aiButton.innerHTML; aiButton.innerHTML = '⏳ AI...'; aiButton.disabled = true; const aiResponse = await callAI(promptText, 'generate', aiButton); // Pass button for disabling if (aiResponse !== null) { let formattedResponse = aiResponse; if (aiResponse.startsWith('(')) { // Handle error/blocked messages formattedResponse = `\n\n--- ${aiResponse} --- \n\n`; } else { formattedResponse = aiResponse.replace(/\n/g, editorContentArea.isContentEditable ? '<br>' : '\n'); } // Insert response (simple append/replace for now) if (editorContentArea.isContentEditable) { document.execCommand('insertHTML', false, (formattedResponse.includes('<br>') ? formattedResponse : '<p>' + formattedResponse + '</p>')); } else if (editorContentArea.value !== undefined) { editorContentArea.value += '\n\n' + formattedResponse; } } // Restore button state (already handled by callAI if button passed) aiButton.innerHTML = originalButtonText; // aiButton.disabled = false; // This is handled within callAI now } }); const lastButtonGroup = toolbar.querySelector('.fr-btn-group:last-child') || toolbar; lastButtonGroup.appendChild(aiButton); } } // --- Global Event Listeners --- /** Handles mouse up events to detect text selection for context menu */ function handleMouseUp(event) { // Don't show context menu if clicking inside our own UI elements if (event.target.closest('#br-ai-settings-panel, #br-ai-context-menu, #br-ai-result-modal, #br-ai-toggle-icon')) { return; } // Debounce or delay slightly to ensure selection is finalized setTimeout(() => { const selection = window.getSelection(); const selectedText = selection ? selection.toString().trim() : ''; if (selectedText && selectedText.length > 5) { // Require minimum length currentSelection = { text: selectedText, range: selection.getRangeAt(0) }; // Calculate position near the end of the selection const rect = currentSelection.range.getBoundingClientRect(); const posX = event.clientX; // Use mouse X const posY = rect.bottom + window.scrollY + 5; // Position below selection rect end showContextMenu(posX, posY); } else { currentSelection = null; hideContextMenu(); } }, 50); // Small delay } /** Handles clicks outside UI elements to hide them */ function handleMouseDown(event) { // Hide context menu if clicking outside it if (contextMenuVisible && !event.target.closest('#br-ai-context-menu')) { hideContextMenu(); } // Hide settings panel if clicking outside it AND outside the toggle icon if (settingsPanelVisible && !event.target.closest('#br-ai-settings-panel') && event.target !== toggleIcon) { toggleSettingsPanel(); // Use toggle to handle visibility state } // Note: Modal closes via its own close button or backdrop click, handled separately } // --- Initialization --- function initialize() { console.log("Gemini AI Helper v0.6 Initializing..."); injectStyles(); createSettingsPanel(); createContextMenu(); // Create context menu structure createResultModal(); // Create result modal structure // Add global listeners document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mousedown', handleMouseDown); // Use mousedown to hide before potential mouseup selection // Attempt to add editor button (using observer is more robust) const observer = new MutationObserver((mutationsList) => { for(let mutation of mutationsList) { if (mutation.type === 'childList') { if (document.querySelector('.fr-toolbar') && !document.querySelector('.br-ai-editor-button')) { addAiButtonToEditor(); } } } }); observer.observe(document.body, { childList: true, subtree: true }); // Fallback checks for editor button setTimeout(addAiButtonToEditor, 1500); setTimeout(addAiButtonToEditor, 4000); // Initial API key check (silent) setTimeout(() => checkApiKey(true), 500); console.log("Gemini AI Helper Initialized."); } // --- Run Script --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();