// ==UserScript==
// @name Naurok AI Answer Checker
// @name:uk Naurok AI Перевірка Відповідей
// @name:ru Naurok AI Проверка Ответов
// @namespace https://greasyfork.org/ru/users/1438166-endervano
// @version 1.0.1
// @description Uses AI to check answers on Naurok. Supports multiple subjects and languages.
// @description:uk Використовує ШІ для перевірки відповідей на Naurok. Підтримує різні предмети та мови.
// @description:ru Использует ИИ для проверки ответов на Naurok. Поддерживает разные предметы и языки.
// @author ENDERVANO
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=naurok.com.ua
// @match *://naurok.com.ua/test/testing/*
// @match *://naurok.com.ua/test/realtime-client/*
// @include *://naurok.com.ua/*
// @grant GM_xmlhttpRequest
// @connect generativelanguage.googleapis.com
// @connect naurok-test2.nyc3.digitaloceanspaces.com
// @connect *
// ==/UserScript==
(function() {
'use strict';
// Добавляем опции выбора моделей
const models = [
{ id: 'custom', name: 'Указать ID модели из OpenRouter' },
{ id: 'gemini-2.5-pro-preview', name: '🔄 Gemini 2.5 Pro Preview (лучшая)' },
{ id: 'gemini-2.0-flash', name: '🔄 Gemini 2.0 Flash (быстрая)' },
{ id: 'gemini-1.5-pro', name: '🔄 Gemini 1.5 Pro (универсальная)' },
{ id: 'gemini-1.5-flash', name: '🔄 Gemini 1.5 Flash (легкая)' },
{ id: 'io-intelligence', name: '🧠 IO Intelligence (бесплатные мощные модели)' }
];
function getApiKey() {
return localStorage.getItem('openrouter_api_key');
}
function getGeminiApiKey() {
return localStorage.getItem('gemini_api_key') || '';
}
function setGeminiApiKey(key) {
localStorage.setItem('gemini_api_key', key);
return true;
}
function getCustomModel() {
return localStorage.getItem('custom_model') || 'google/gemini-pro:free';
}
function setCustomModel(model) {
localStorage.setItem('custom_model', model);
return true;
}
function validateApiKey(key) {
if (!key) return false;
if (key.length < 10) return false;
return true;
}
function setApiKey(key) {
if (!validateApiKey(key)) {
alert('API ключ должен быть длиннее 10 символов');
return false;
}
localStorage.setItem('openrouter_api_key', key);
return true;
}
function createSettingsModal() {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 10000;
width: 450px;
max-height: 90vh;
overflow-y: auto;
`;
const title = document.createElement('h3');
title.textContent = 'Настройки AI проверки';
title.style.marginBottom = '15px';
// Создаем переключатель моделей
const modeLabel = document.createElement('div');
modeLabel.textContent = 'Выберите модель AI:';
modeLabel.style.cssText = `
margin-bottom: 8px;
font-weight: bold;
`;
const modeSelect = document.createElement('select');
modeSelect.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Добавляем опции выбора
models.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
if (model.id === getSelectedModel()) {
option.selected = true;
}
modeSelect.appendChild(option);
});
// Контейнер для OpenRouter
const openRouterContainer = document.createElement('div');
openRouterContainer.style.cssText = `
margin-bottom: 15px;
display: ${getSelectedModel() === 'custom' ? 'block' : 'none'};
`;
// Инструкции для OpenRouter
const openRouterInstructions = document.createElement('div');
openRouterInstructions.style.cssText = `
background: #e3f2fd;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
`;
openRouterInstructions.innerHTML = `
<p style="margin: 0 0 10px 0"><strong>📝 Как получить API ключ OpenRouter:</strong></p>
<ol style="margin: 0; padding-left: 20px;">
<li>Перейдите на <a href="https://openrouter.ai/keys" target="_blank">OpenRouter</a></li>
<li>Зарегистрируйтесь или войдите в аккаунт</li>
<li>Создайте новый API ключ, нажав "Create Key"</li>
<li>Скопируйте сгенерированный ключ</li>
<li>Вставьте ключ в поле ниже</li>
</ol>
`;
// ID модели для OpenRouter
const customModelLabel = document.createElement('div');
customModelLabel.textContent = 'ID модели с OpenRouter:';
customModelLabel.style.cssText = `
margin-bottom: 8px;
font-weight: bold;
`;
const customModelInput = document.createElement('input');
customModelInput.type = 'text';
customModelInput.placeholder = 'Например: google/gemini-pro:free';
customModelInput.value = getCustomModel() || '';
customModelInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Информация о моделях OpenRouter
const modelInfo = document.createElement('div');
modelInfo.style.cssText = `
background: #fff3e0;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 13px;
`;
modelInfo.innerHTML = `
<p style="margin: 0 0 5px 0"><strong>ℹ️ Примеры ID моделей OpenRouter:</strong></p>
<ul style="margin: 0; padding-left: 20px;">
<li><code>google/gemini-pro:free</code> - Gemini Pro (бесплатная)</li>
<li><code>anthropic/claude-instant-1.2:free</code> - Claude Instant (бесплатная)</li>
<li><code>mistralai/mistral-7b-instruct:free</code> - Mistral 7B (бесплатная)</li>
<li><code>meta-llama/llama-3-8b-instruct:free</code> - Llama 3 8B (бесплатная)</li>
</ul>
`;
// API ключ для OpenRouter
const apiKeyLabel = document.createElement('div');
apiKeyLabel.textContent = 'API ключ OpenRouter:';
apiKeyLabel.style.cssText = `
margin-top: 15px;
margin-bottom: 8px;
font-weight: bold;
`;
const apiKeyInput = document.createElement('input');
apiKeyInput.type = 'password';
apiKeyInput.placeholder = 'Введите ваш OpenRouter API ключ';
apiKeyInput.value = getApiKey() || '';
apiKeyInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Кнопка показать/скрыть пароль
const togglePassword = document.createElement('button');
togglePassword.textContent = '👁️ Показать ключ';
togglePassword.style.cssText = `
padding: 5px 10px;
background: none;
border: none;
color: #007BFF;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
`;
togglePassword.onclick = () => {
if (apiKeyInput.type === 'password') {
apiKeyInput.type = 'text';
togglePassword.textContent = '👁️ Скрыть ключ';
} else {
apiKeyInput.type = 'password';
togglePassword.textContent = '👁️ Показать ключ';
}
};
// Собираем контейнер OpenRouter
openRouterContainer.appendChild(openRouterInstructions);
openRouterContainer.appendChild(customModelLabel);
openRouterContainer.appendChild(customModelInput);
openRouterContainer.appendChild(modelInfo);
openRouterContainer.appendChild(apiKeyLabel);
openRouterContainer.appendChild(apiKeyInput);
openRouterContainer.appendChild(togglePassword);
// Контейнер для Gemini
const geminiContainer = document.createElement('div');
geminiContainer.style.cssText = `
margin-bottom: 15px;
display: ${getSelectedModel().startsWith('gemini') ? 'block' : 'none'};
`;
// Предупреждение о Gemini
const geminiWarning = document.createElement('div');
geminiWarning.style.cssText = `
background: #ffecb3;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
`;
geminiWarning.innerHTML = `
<p style="margin: 0 0 10px 0"><strong>⚠️ Внимание! Для Gemini требуется API ключ из Google AI Studio</strong></p>
<p>API ключ Gemini отличается от OpenRouter и получается из другого сервиса!</p>
`;
// Инструкции для Gemini
const geminiInstructions = document.createElement('div');
geminiInstructions.style.cssText = `
background: #e3f2fd;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
`;
geminiInstructions.innerHTML = `
<p style="margin: 0 0 10px 0"><strong>📝 Как получить API ключ Gemini:</strong></p>
<ol style="margin: 0; padding-left: 20px;">
<li>Перейдите на <a href="https://makersuite.google.com/app/apikey" target="_blank">Google AI Studio</a></li>
<li>Войдите в аккаунт Google (если еще не вошли)</li>
<li>Нажмите "Create API key" (Создать API ключ)</li>
<li>Если появится окно с условиями использования, примите их</li>
<li>Система сгенерирует ключ - скопируйте его</li>
<li>Вставьте ключ в поле ниже</li>
</ol>
<p style="margin: 10px 0 0 0"><strong>💡 Доступные модели Gemini:</strong></p>
<ul style="margin: 5px 0 0 20px; padding: 0;">
<li><strong>Gemini 2.5 Pro</strong> - Самая продвинутая модель (поддерживает работу с изображениями, аудио, видео)</li>
<li><strong>Gemini 2.0 Flash</strong> - Быстрая модель с хорошим балансом возможностей</li>
<li><strong>Gemini 1.5 Pro</strong> - Универсальная модель для разных задач</li>
<li><strong>Gemini 1.5 Flash</strong> - Легкая модель для простых задач</li>
</ul>
`;
// API ключ для Gemini
const geminiKeyLabel = document.createElement('div');
geminiKeyLabel.textContent = 'API ключ Google AI Studio:';
geminiKeyLabel.style.cssText = `
margin-top: 15px;
margin-bottom: 8px;
font-weight: bold;
`;
const geminiKeyInput = document.createElement('input');
geminiKeyInput.type = 'password';
geminiKeyInput.placeholder = 'Введите ваш Google AI Studio API ключ';
geminiKeyInput.value = getGeminiApiKey() || '';
geminiKeyInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Кнопка показать/скрыть пароль для Gemini
const toggleGeminiPassword = document.createElement('button');
toggleGeminiPassword.textContent = '👁️ Показать ключ';
toggleGeminiPassword.style.cssText = `
padding: 5px 10px;
background: none;
border: none;
color: #007BFF;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
`;
toggleGeminiPassword.onclick = () => {
if (geminiKeyInput.type === 'password') {
geminiKeyInput.type = 'text';
toggleGeminiPassword.textContent = '👁️ Скрыть ключ';
} else {
geminiKeyInput.type = 'password';
toggleGeminiPassword.textContent = '👁️ Показать ключ';
}
};
// Собираем контейнер Gemini
geminiContainer.appendChild(geminiWarning);
geminiContainer.appendChild(geminiInstructions);
geminiContainer.appendChild(geminiKeyLabel);
geminiContainer.appendChild(geminiKeyInput);
geminiContainer.appendChild(toggleGeminiPassword);
// Добавляем контейнер для IO Intelligence
const ioIntelligenceContainer = document.createElement('div');
ioIntelligenceContainer.style.cssText = `
margin-bottom: 15px;
display: ${getSelectedModel() === 'io-intelligence' ? 'block' : 'none'};
`;
// Инструкции для IO Intelligence
const ioIntelligenceInstructions = document.createElement('div');
ioIntelligenceInstructions.style.cssText = `
background: #e3f2fd;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 14px;
`;
ioIntelligenceInstructions.innerHTML = `
<p style="margin: 0 0 10px 0"><strong>📝 Как получить API ключ IO Intelligence:</strong></p>
<ol style="margin: 0; padding-left: 20px;">
<li>Перейдите на <a href="https://intelligence.io.solutions/" target="_blank">IO Intelligence</a></li>
<li>Зарегистрируйтесь или войдите в аккаунт</li>
<li>В личном кабинете создайте API ключ</li>
<li>Скопируйте сгенерированный ключ</li>
<li>Вставьте ключ в поле ниже</li>
</ol>
<p style="margin: 10px 0 0 0"><strong>💡 Преимущества IO Intelligence:</strong></p>
<ul style="margin: 5px 0 0 20px; padding: 0;">
<li>Бесплатный доступ к мощным моделям</li>
<li>Лимит 1,000,000 токенов в день для каждой модели</li>
<li>Поддержка контекста до 128,000 токенов</li>
</ul>
`;
// Выбор модели IO Intelligence
const ioIntelligenceModelLabel = document.createElement('div');
ioIntelligenceModelLabel.textContent = 'Выберите модель IO Intelligence:';
ioIntelligenceModelLabel.style.cssText = `
margin-bottom: 8px;
font-weight: bold;
`;
const ioIntelligenceModelSelect = document.createElement('select');
ioIntelligenceModelSelect.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Список моделей IO Intelligence
const ioIntelligenceModels = [
{ id: 'meta-llama/Llama-3.3-70B-Instruct', name: 'Meta Llama 3.3 70B (лучшая)' },
{ id: 'deepseek-ai/DeepSeek-R1', name: 'DeepSeek R1' },
{ id: 'mistralai/Mistral-Large-Instruct-2411', name: 'Mistral Large 2411' },
{ id: 'Qwen/Qwen2.5-Coder-32B-Instruct', name: 'Qwen 2.5 Coder 32B' },
{ id: 'Qwen/QwQ-32B-Preview', name: 'Qwen QwQ 32B' },
{ id: 'databricks/dbrx-instruct', name: 'Databricks DBRX' },
{ id: 'microsoft/phi-4', name: 'Microsoft Phi-4' },
{ id: 'THUDM/glm-4-9b-chat', name: 'GLM-4 9B' },
{ id: 'microsoft/Phi-3.5-mini-instruct', name: 'Microsoft Phi-3.5 mini' }
];
// Добавляем опции моделей
ioIntelligenceModels.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
if (model.id === getIOIntelligenceModel()) {
option.selected = true;
}
ioIntelligenceModelSelect.appendChild(option);
});
// API ключ для IO Intelligence
const ioIntelligenceKeyLabel = document.createElement('div');
ioIntelligenceKeyLabel.textContent = 'API ключ IO Intelligence:';
ioIntelligenceKeyLabel.style.cssText = `
margin-top: 15px;
margin-bottom: 8px;
font-weight: bold;
`;
const ioIntelligenceKeyInput = document.createElement('input');
ioIntelligenceKeyInput.type = 'password';
ioIntelligenceKeyInput.placeholder = 'Введите ваш IO Intelligence API ключ';
ioIntelligenceKeyInput.value = getIOIntelligenceApiKey() || '';
ioIntelligenceKeyInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
`;
// Кнопка показать/скрыть пароль для IO Intelligence
const toggleIOIntelligencePassword = document.createElement('button');
toggleIOIntelligencePassword.textContent = '👁️ Показать ключ';
toggleIOIntelligencePassword.style.cssText = `
padding: 5px 10px;
background: none;
border: none;
color: #007BFF;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
`;
toggleIOIntelligencePassword.onclick = () => {
if (ioIntelligenceKeyInput.type === 'password') {
ioIntelligenceKeyInput.type = 'text';
toggleIOIntelligencePassword.textContent = '👁️ Скрыть ключ';
} else {
ioIntelligenceKeyInput.type = 'password';
toggleIOIntelligencePassword.textContent = '👁️ Показать ключ';
}
};
// Собираем контейнер IO Intelligence
ioIntelligenceContainer.appendChild(ioIntelligenceInstructions);
ioIntelligenceContainer.appendChild(ioIntelligenceModelLabel);
ioIntelligenceContainer.appendChild(ioIntelligenceModelSelect);
ioIntelligenceContainer.appendChild(ioIntelligenceKeyLabel);
ioIntelligenceContainer.appendChild(ioIntelligenceKeyInput);
ioIntelligenceContainer.appendChild(toggleIOIntelligencePassword);
// Изменяем функцию обработчика изменения
modeSelect.addEventListener('change', function() {
openRouterContainer.style.display = this.value === 'custom' ? 'block' : 'none';
geminiContainer.style.display = this.value.startsWith('gemini') ? 'block' : 'none';
ioIntelligenceContainer.style.display = this.value === 'io-intelligence' ? 'block' : 'none';
});
// Добавляем поле для дополнительного контекста
const contextLabel = document.createElement('div');
contextLabel.textContent = 'Дополнительный контекст (необязательно):';
contextLabel.style.cssText = `
margin-top: 15px;
margin-bottom: 8px;
font-weight: bold;
`;
const contextTextarea = document.createElement('textarea');
contextTextarea.placeholder = 'Введите дополнительную информацию для AI (предмет, тема, ключевые термины)';
contextTextarea.value = getAdditionalContext() || '';
contextTextarea.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-height: 80px;
resize: vertical;
`;
// Добавляем кнопки
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
`;
const saveButton = document.createElement('button');
saveButton.textContent = 'Сохранить';
saveButton.style.cssText = `
padding: 8px 15px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Отмена';
cancelButton.style.cssText = `
padding: 8px 15px;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
buttonContainer.appendChild(saveButton);
buttonContainer.appendChild(cancelButton);
// Добавляем все элементы в модальное окно
modal.appendChild(title);
modal.appendChild(modeLabel);
modal.appendChild(modeSelect);
modal.appendChild(openRouterContainer);
modal.appendChild(geminiContainer);
modal.appendChild(ioIntelligenceContainer);
modal.appendChild(contextLabel);
modal.appendChild(contextTextarea);
modal.appendChild(buttonContainer);
// Добавляем обработчики событий
saveButton.onclick = () => {
let isValid = true;
if (modeSelect.value === 'custom') {
if (!validateApiKey(apiKeyInput.value)) {
alert('Введите корректный API ключ OpenRouter');
isValid = false;
}
if (customModelInput.value.trim() === '') {
alert('Введите ID модели OpenRouter');
isValid = false;
}
} else if (modeSelect.value.startsWith('gemini')) {
if (!validateApiKey(geminiKeyInput.value)) {
alert('Введите корректный API ключ Google AI Studio');
isValid = false;
}
} else if (modeSelect.value === 'io-intelligence') {
if (!validateApiKey(ioIntelligenceKeyInput.value)) {
alert('Введите корректный API ключ IO Intelligence');
isValid = false;
}
}
if (isValid) {
if (modeSelect.value === 'custom') {
setApiKey(apiKeyInput.value);
setCustomModel(customModelInput.value.trim());
} else if (modeSelect.value.startsWith('gemini')) {
setGeminiApiKey(geminiKeyInput.value);
} else if (modeSelect.value === 'io-intelligence') {
setIOIntelligenceApiKey(ioIntelligenceKeyInput.value);
setIOIntelligenceModel(ioIntelligenceModelSelect.value);
}
setSelectedModel(modeSelect.value);
setAdditionalContext(contextTextarea.value);
document.body.removeChild(modal);
// Показываем уведомление об успешном сохранении
const successMessage = document.createElement('div');
successMessage.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #4CAF50;
color: white;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
successMessage.textContent = 'Настройки успешно сохранены';
document.body.appendChild(successMessage);
setTimeout(() => successMessage.remove(), 3000);
}
};
cancelButton.onclick = () => {
document.body.removeChild(modal);
};
document.body.appendChild(modal);
}
// Фиксируем функцию askAI для работы только с одной моделью
async function askAI(prompt, images = []) {
const mode = getSelectedModel();
if (mode === 'custom') {
return askOpenRouter(prompt, images);
} else if (mode.startsWith('gemini')) {
return askGemini(prompt, images, mode);
} else if (mode === 'io-intelligence') {
return askIOIntelligence(prompt, images);
} else {
return askOpenRouter(prompt, images);
}
}
// Функция для запроса к OpenRouter
async function askOpenRouter(prompt, images = []) {
const apiKey = getApiKey();
if (!apiKey) {
createSettingsModal();
throw new Error('API ключ OpenRouter не настроен');
}
// Получаем модель напрямую из хранилища
const modelId = getCustomModel();
if (!modelId) {
throw new Error('Не указана модель OpenRouter');
}
// Правильно формируем сообщения для API
let messages = [
{
role: 'system',
content: `You are an expert test-solving AI. Your task is to analyze the question and options carefully and determine the correct answer(s).
Answer with ONLY the number(s) of the correct option(s) (e.g., "1" for single choice or "1,3" for multiple choice).
Do not provide explanations or additional text, just the answer number(s).`
}
];
// Формируем содержимое сообщения пользователя
let userContent = [];
// Добавляем изображения если они есть
if (images.length > 0) {
images.forEach(image => {
userContent.push(image);
});
}
// Добавляем текст промпта
userContent.push({
type: 'text',
text: prompt
});
// Добавляем сообщение пользователя
messages.push({
role: 'user',
content: userContent
});
return new Promise((resolve, reject) => {
// Отображаем индикатор загрузки
const loadingIndicator = document.createElement('div');
loadingIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #2196F3;
color: white;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
loadingIndicator.textContent = '🤖 Запрос к OpenRouter...';
document.body.appendChild(loadingIndicator);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://openrouter.ai/api/v1/chat/completions',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://naurok.com.ua',
'X-Title': 'Naurok AI Checker'
},
data: JSON.stringify({
model: modelId,
messages: messages,
temperature: 0.1,
max_tokens: 100,
top_p: 0.1,
frequency_penalty: 0,
presence_penalty: 0,
stop: ["\n", ".", " ", "Answer", "The", "I", "Based"]
}),
onload: function(response) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
try {
const data = JSON.parse(response.responseText);
console.log('API Response:', data);
if (data.error) {
reject(new Error(data.error.message || 'Ошибка API OpenRouter'));
return;
}
if (data.choices && data.choices[0] && data.choices[0].message) {
let text = data.choices[0].message.content.trim();
console.log('Raw response:', text);
// Извлекаем только цифры и запятые из ответа
const cleanedText = text.replace(/[^0-9,]/g, '');
if (cleanedText) {
resolve(cleanedText);
} else {
// Если не удалось извлечь числа, ищем их в исходном тексте
const matches = text.match(/[1-4]/g);
if (matches) {
resolve(matches.join(','));
} else {
reject(new Error('Не удалось получить числовой ответ'));
}
}
} else {
reject(new Error('Некорректный ответ от API'));
}
} catch (error) {
console.error('Ошибка обработки ответа:', error);
reject(new Error('Ошибка обработки ответа API'));
}
},
onerror: function(error) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
console.error('Ошибка запроса:', error);
reject(new Error('Ошибка сетевого запроса к OpenRouter'));
}
});
});
}
// Функция для запроса к Gemini
async function askGemini(prompt, images = [], modelType) {
const apiKey = getGeminiApiKey();
if (!apiKey) {
createSettingsModal();
throw new Error('API ключ Gemini не настроен');
}
// Получаем правильный ID модели
const modelId = getGeminiModelId(modelType);
// Формируем правильный промпт для Gemini (без ролей)
const geminiPrompt = {
contents: [
{
parts: [
{
text: `Ты - AI-помощник для решения тестов. Проанализируй вопрос и варианты ответов и определи правильный вариант.
ВАЖНОЕ ПРАВИЛО: Отвечай ТОЛЬКО номером правильного ответа (например, "1" для одиночного выбора или "1,3" для множественного). Без объяснений, без дополнительного текста, только номер(а).
${prompt}`
}
]
}
]
};
// Если есть изображения, добавляем только первое
if (images.length > 0) {
try {
const base64Image = images[0].image_url.url.split(',')[1];
if (base64Image) {
geminiPrompt.contents[0].parts.unshift({
inline_data: {
mime_type: "image/jpeg",
data: base64Image
}
});
}
} catch (error) {
console.error('Ошибка добавления изображения:', error);
}
}
return new Promise((resolve, reject) => {
// Отображаем индикатор загрузки
const loadingIndicator = document.createElement('div');
loadingIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #2196F3;
color: white;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
loadingIndicator.textContent = '🤖 Запрос к Gemini...';
document.body.appendChild(loadingIndicator);
GM_xmlhttpRequest({
method: 'POST',
url: `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${apiKey}`,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
contents: geminiPrompt.contents,
generationConfig: {
temperature: 0.1,
maxOutputTokens: 100,
topP: 0.1,
topK: 1
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_NONE"
}
]
}),
onload: function(response) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
try {
const data = JSON.parse(response.responseText);
console.log('Gemini Response:', data);
if (data.error) {
reject(new Error(data.error.message || 'Ошибка API Gemini'));
return;
}
if (data.candidates && data.candidates[0] && data.candidates[0].content) {
let text = data.candidates[0].content.parts[0].text.trim();
console.log('Raw Gemini response:', text);
// Извлекаем только цифры и запятые из ответа
const cleanedText = text.replace(/[^0-9,]/g, '');
if (cleanedText) {
resolve(cleanedText);
} else {
// Если не удалось извлечь числа, ищем их в исходном тексте
const matches = text.match(/[1-4]/g);
if (matches) {
resolve(matches.join(','));
} else {
reject(new Error('Не удалось получить числовой ответ от Gemini'));
}
}
} else {
reject(new Error('Некорректный ответ от Gemini API'));
}
} catch (error) {
console.error('Ошибка обработки ответа Gemini:', error);
reject(new Error('Ошибка обработки ответа Gemini API'));
}
},
onerror: function(error) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
console.error('Ошибка запроса к Gemini:', error);
reject(new Error('Ошибка сетевого запроса к Gemini'));
}
});
});
}
// Функция для работы с изображениями
async function fetchImage(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: function(response) {
try {
const reader = new FileReader();
reader.onloadend = () => {
const base64Data = reader.result.split(',')[1];
resolve({
type: 'image',
image_url: {
url: `data:image/jpeg;base64,${base64Data}`
}
});
};
reader.readAsDataURL(response.response);
} catch (error) {
console.error('Ошибка конвертации изображения:', error);
reject(error);
}
},
onerror: function(error) {
console.error('Ошибка загрузки изображения:', error);
reject(error);
}
});
});
}
function highlightCorrectAnswers(correctIndices) {
// Находим все варианты ответов
const options = document.querySelectorAll('.test-option, .question-option');
options.forEach((option, index) => {
if (correctIndices.includes(index + 1)) {
const innerContent = option.querySelector('.question-option-inner, .question-option-inner-content, .question-option-content');
if (innerContent) {
// Добавляем только рамку и легкий фон
innerContent.style.border = '2px solid #4CAF50';
innerContent.style.backgroundColor = 'rgba(76, 175, 80, 0.1)';
// Добавляем метку AI, если её еще нет
if (!option.querySelector('.ai-label')) {
const label = document.createElement('div');
label.className = 'ai-label';
label.style.cssText = `
position: absolute;
right: 8px;
top: 8px;
background: #4CAF50;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
`;
label.textContent = '🤖 AI';
option.appendChild(label);
}
}
}
});
}
// Функция для очистки результатов предыдущих проверок
function clearPreviousResults() {
// Удаляем все метки AI
document.querySelectorAll('.ai-label').forEach(el => el.remove());
// Удаляем все уведомления и индикаторы
document.querySelectorAll('.ai-notification, .ai-warning, .ai-indicator').forEach(el => el.remove());
// Очищаем стили с вариантов ответов
document.querySelectorAll('.question-option-inner, .test-option').forEach(option => {
option.style.removeProperty('border');
option.style.removeProperty('background-color');
option.style.removeProperty('box-shadow');
option.style.removeProperty('transform');
});
// Очищаем внутренние элементы вариантов ответов
document.querySelectorAll('.question-option-inner-content, .question-option-content').forEach(content => {
content.style.removeProperty('border');
content.style.removeProperty('background-color');
content.style.removeProperty('box-shadow');
});
// Удаляем кнопку AI, чтобы она пересоздалась для нового вопроса
const aiButton = document.querySelector('#aiCheckButton');
if (aiButton) {
const buttonContainer = aiButton.closest('div');
if (buttonContainer) {
buttonContainer.remove();
}
}
}
async function checkAnswersWithAI() {
const questionElem = document.querySelector('.test-content-text-inner, .test-question-content-inner');
if (!questionElem) {
console.log('Вопрос еще не загружен');
return;
}
try {
clearPreviousResults();
// Определяем тип вопроса
const isSingleChoice = !document.querySelector('.question-option-inner-multiple');
const question = questionElem.innerText.trim();
// Собираем варианты ответов
const options = Array.from(document.querySelectorAll('.test-option, .question-option'))
.map((el, index) => {
const contentEl = el.querySelector('.question-option-inner-content, .question-option-content');
const text = contentEl?.innerText.trim() || '';
return `${index + 1}) ${text}`;
})
.filter(text => text.length > 0)
.join('\n');
// Собираем изображения с использованием GM_xmlhttpRequest
const images = [];
const imageElements = document.querySelectorAll('.test-content-image img, .question-option-image');
for (const imgElement of imageElements) {
try {
let imgUrl = imgElement.src || imgElement.style.backgroundImage?.replace(/url\(['"](.+)['"]\)/, '$1');
if (!imgUrl) continue;
// Используем новую функцию для загрузки изображения
const base64data = await fetchImage(imgUrl);
if (base64data) {
images.push(base64data);
}
} catch (error) {
console.error('Ошибка при обработке изображения:', error);
}
}
// Формируем улучшенный промпт
const prompt = createPrompt(question, options, isSingleChoice, images);
const response = await askAI(prompt, images);
// Обработка ответа
let correctIndices = response.split(',')
.map(num => parseInt(num.trim()))
.filter(num => !isNaN(num));
if (isSingleChoice && correctIndices.length > 1) {
correctIndices = [correctIndices[0]];
}
if (correctIndices.length === 0) {
throw new Error('AI не смог определить правильный ответ');
}
// Подсвечиваем ответы
highlightCorrectAnswers(correctIndices);
// Показываем индикатор
const container = document.querySelector('.test-question-options, .test-options-grid');
if (container) {
const indicator = document.createElement('div');
indicator.classList.add('ai-indicator');
indicator.style.cssText = `
margin: 15px;
padding: 10px;
background-color: #4CAF50;
color: white;
border-radius: 5px;
font-size: 14px;
text-align: center;
position: relative;
z-index: 1000;
`;
indicator.textContent = `🤖 AI считает правильным${correctIndices.length > 1 ? 'и' : ''} вариант${correctIndices.length > 1 ? 'ы' : ''}: ${correctIndices.join(', ')}`;
container.appendChild(indicator);
}
} catch (error) {
console.error('Ошибка при запросе к AI:', error);
const errorMessage = document.createElement('div');
errorMessage.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #ff5252;
color: white;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
errorMessage.textContent = `Ошибка: ${error.message}`;
document.body.appendChild(errorMessage);
setTimeout(() => errorMessage.remove(), 5000);
}
}
function addCheckButton() {
// Ищем контейнер для кнопки (поддержка обоих версий сайта)
const questionContainer = document.querySelector('.test-question-options, .test-options-grid');
if (!questionContainer || document.querySelector('#aiCheckButton')) return;
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
margin: 15px;
position: relative;
z-index: 1000;
padding: 0 15px;
`;
const checkButton = document.createElement('button');
checkButton.id = 'aiCheckButton';
checkButton.textContent = '🤖 Проверить с помощью AI';
checkButton.style.cssText = `
flex: 1;
padding: 12px 20px;
background: #f48617;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;
const settingsButton = document.createElement('button');
settingsButton.textContent = '⚙️';
settingsButton.style.cssText = `
padding: 12px 20px;
background: #5362c2;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;
// Добавляем эффекты при наведении
checkButton.onmouseover = () => checkButton.style.background = '#e67b15';
checkButton.onmouseout = () => checkButton.style.background = '#f48617';
settingsButton.onmouseover = () => settingsButton.style.background = '#4552a3';
settingsButton.onmouseout = () => settingsButton.style.background = '#5362c2';
checkButton.onclick = checkAnswersWithAI;
settingsButton.onclick = createSettingsModal;
buttonContainer.appendChild(checkButton);
buttonContainer.appendChild(settingsButton);
// Добавляем контейнер с кнопками
questionContainer.insertAdjacentElement('beforebegin', buttonContainer);
}
function init() {
let isProcessing = false;
let lastQuestionNumber = null;
const tryAddButton = () => {
if (document.querySelector('.test-question-options, .test-options-grid') && !document.querySelector('#aiCheckButton')) {
addCheckButton();
}
};
// Функция для обработки изменений с защитой от повторных вызовов
const handleChanges = () => {
if (isProcessing) return;
isProcessing = true;
// Получаем текущий номер вопроса
const currentQuestionNumber = document.querySelector('.currentActiveQuestion')?.textContent;
// Проверяем, изменился ли номер вопроса
if (currentQuestionNumber && currentQuestionNumber !== lastQuestionNumber) {
lastQuestionNumber = currentQuestionNumber;
clearPreviousResults();
tryAddButton();
}
isProcessing = false;
};
// Запускаем первую попытку
setTimeout(tryAddButton, 1000);
// Наблюдаем за изменениями в DOM
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const hasRelevantChanges = mutation.target.classList?.contains('test-content-text-inner') ||
mutation.target.classList?.contains('test-question-content-inner') ||
mutation.target.classList?.contains('test-options-grid') ||
mutation.target.classList?.contains('test-question-options');
if (hasRelevantChanges) {
handleChanges();
break;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Добавляем обработчик для Angular
if (window.angular) {
const scope = angular.element(document.querySelector('[ng-controller]')).scope();
if (scope) {
scope.$watch('test.session.answers', () => {
handleChanges();
});
}
}
// Периодическая проверка наличия кнопки (реже)
setInterval(tryAddButton, 3000);
}
// Запускаем инициализацию
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// Функция для получения и установки выбранной модели
function getSelectedModel() {
return localStorage.getItem('selected_ai_model') || 'google/gemini-pro:free';
}
function setSelectedModel(modelId) {
localStorage.setItem('selected_ai_model', modelId);
return true;
}
// Функция для получения и установки дополнительного контекста
function getAdditionalContext() {
return localStorage.getItem('additional_context') || '';
}
function setAdditionalContext(context) {
localStorage.setItem('additional_context', context);
return true;
}
// Функция для создания промпта
function createPrompt(question, options, isSingleChoice, images = []) {
const additionalContext = getAdditionalContext();
// Создаем базовый промпт
let prompt = `
ВОПРОС:
${question}
ВАРИАНТЫ ОТВЕТОВ:
${options}
`;
// Добавляем инструкции про изображения если они есть
if (images.length > 0) {
prompt += `
ВАЖНО: К вопросу прикреплены изображения. Внимательно изучи их и используй информацию с изображений при выборе ответа.
`;
}
// Добавляем дополнительный контекст если он есть
if (additionalContext) {
prompt += `
ДОПОЛНИТЕЛЬНЫЙ КОНТЕКСТ:
${additionalContext}
`;
}
// Добавляем инструкции по формату ответа
prompt += `
ИНСТРУКЦИИ:
1. Внимательно прочитай вопрос и все варианты ответов
2. Обрати внимание на ключевые слова и фразы
3. Если к вопросу прикреплены изображения, изучи их тщательно
4. Используй логику и знания предмета для определения правильного ответа
5. Ответ нужен ТОЛЬКО в виде номера/номеров без каких-либо пояснений
6. ${isSingleChoice ? 'Выбери ОДИН правильный вариант' : 'Можно выбрать НЕСКОЛЬКО вариантов, если несколько ответов верны'}
ФОРМАТ ОТВЕТА: Только номер(а) правильного ответа. Например: "2" или "1,3" для множественного выбора.
Без пояснений, без дополнительного текста, только цифры!
`;
return prompt;
}
// Получаем ID модели Gemini на основе выбора
function getGeminiModelId(modelId) {
switch(modelId) {
case 'gemini-2.5-pro-preview':
return 'gemini-2.5-pro-preview-03-25';
case 'gemini-2.0-flash':
return 'gemini-2.0-flash';
case 'gemini-1.5-pro':
return 'gemini-1.5-pro';
case 'gemini-1.5-flash':
return 'gemini-1.5-flash';
default:
return 'gemini-1.5-pro'; // Дефолтная модель
}
}
// Добавляем получение и установку API ключа для IO Intelligence
function getIOIntelligenceApiKey() {
return localStorage.getItem('io_intelligence_api_key') || '';
}
function setIOIntelligenceApiKey(key) {
localStorage.setItem('io_intelligence_api_key', key);
return true;
}
// Добавляем получение и установку выбранной модели IO Intelligence
function getIOIntelligenceModel() {
return localStorage.getItem('io_intelligence_model') || 'meta-llama/Llama-3.3-70B-Instruct';
}
function setIOIntelligenceModel(model) {
localStorage.setItem('io_intelligence_model', model);
return true;
}
// Исправленная функция запроса к IO Intelligence с корректной авторизацией
async function askIOIntelligence(prompt, images = []) {
const apiKey = getIOIntelligenceApiKey();
if (!apiKey) {
createSettingsModal();
throw new Error('API ключ IO Intelligence не настроен');
}
// Получаем выбранную модель IO Intelligence
const modelId = getIOIntelligenceModel();
if (!modelId) {
throw new Error('Не указана модель IO Intelligence');
}
// Формируем сообщения для API в формате OpenAI
const messages = [
{
role: 'system',
content: `You are an expert test-solving AI. Analyze the question and options carefully to determine the correct answer(s).
Answer with ONLY the number(s) of the correct option(s) (e.g., "1" for single choice or "1,3" for multiple choice).
Do not provide explanations or additional text, just the answer number(s).`
},
{
role: 'user',
content: prompt
}
];
const requestData = {
model: modelId,
messages: messages,
temperature: 0.1,
max_tokens: 100,
top_p: 0.1
};
console.log('IO Intelligence Request:', requestData);
console.log('Using model:', modelId);
console.log('API key length:', apiKey.length, 'First 5 chars:', apiKey.substring(0, 5));
return new Promise((resolve, reject) => {
// Отображаем индикатор загрузки
const loadingIndicator = document.createElement('div');
loadingIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: #2196F3;
color: white;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
loadingIndicator.textContent = '🤖 Запрос к IO Intelligence...';
document.body.appendChild(loadingIndicator);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.intelligence.io.solutions/api/v1/chat/completions',
headers: {
'Authorization': `Bearer ${apiKey.trim()}`, // Убедимся, что нет лишних пробелов
'Content-Type': 'application/json'
},
data: JSON.stringify(requestData),
onload: function(response) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
try {
console.log('IO Intelligence Status Code:', response.status);
console.log('IO Intelligence Headers:', response.responseHeaders);
console.log('IO Intelligence Raw Response:', response.responseText);
if (!response.responseText) {
reject(new Error('Пустой ответ от API IO Intelligence'));
return;
}
// Если в ответе есть упоминание о проблеме аутентификации
if (response.responseText.includes('authentication') ||
response.responseText.includes('Authorization') ||
response.responseText.includes('auth')) {
reject(new Error('Ошибка аутентификации в API IO Intelligence. Проверьте API ключ и попробуйте снова.'));
return;
}
const data = JSON.parse(response.responseText);
// Проверка на наличие ошибки в ответе
if (data.detail) {
reject(new Error(`Ошибка API IO Intelligence: ${data.detail}`));
return;
}
if (data.error) {
reject(new Error(`Ошибка API IO Intelligence: ${typeof data.error === 'string' ? data.error : JSON.stringify(data.error)}`));
return;
}
// Проверяем разные форматы ответа
let text = '';
// Формат OpenAI API
if (data.choices && data.choices.length > 0 && data.choices[0].message) {
text = data.choices[0].message.content.trim();
}
// Другие возможные форматы ответа
else if (data.response) {
text = typeof data.response === 'string' ? data.response.trim() : JSON.stringify(data.response);
}
else if (data.content) {
text = typeof data.content === 'string' ? data.content.trim() : JSON.stringify(data.content);
}
else if (data.text) {
text = data.text.trim();
}
else if (data.answer) {
text = data.answer.trim();
}
else if (data.result) {
text = typeof data.result === 'string' ? data.result.trim() : JSON.stringify(data.result);
}
// Если не удалось найти ответ в известных полях
else {
// Преобразуем весь ответ в строку
text = JSON.stringify(data);
console.log('Using full response as text:', text);
}
if (!text) {
reject(new Error('Не удалось извлечь текст ответа из данных API'));
return;
}
// Извлекаем только цифры и запятые из ответа
const cleanedText = text.replace(/[^0-9,]/g, '');
console.log('Cleaned text:', cleanedText);
if (cleanedText) {
resolve(cleanedText);
} else {
// Если не удалось извлечь числа, ищем их в исходном тексте
const matches = text.match(/[1-4]/g);
console.log('Matched numbers:', matches);
if (matches && matches.length > 0) {
resolve(matches.join(','));
} else {
// Отображаем более детальное сообщение об ошибке
reject(new Error(`Ответ не содержит числовых значений. Проверьте настройки и API ключ. Ответ: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}"`));
}
}
} catch (error) {
console.error('Ошибка обработки ответа IO Intelligence:', error);
// Более детальная диагностика
let errorDetails = '';
if (response && response.status) {
errorDetails += ` HTTP статус: ${response.status}.`;
}
if (response && response.responseText) {
// Проверяем, не содержит ли ответ информацию об ошибке аутентификации
if (response.responseText.includes('"detail":"No authentication present"')) {
errorDetails = ' Ошибка аутентификации: API ключ отсутствует или неверный формат.';
} else {
errorDetails += ` Ответ от сервера: "${response.responseText.substring(0, 200)}${response.responseText.length > 200 ? '...' : ''}"`;
}
}
reject(new Error(`Ошибка обработки ответа: ${error.message}.${errorDetails}`));
}
},
onerror: function(error) {
// Удаляем индикатор загрузки
document.body.removeChild(loadingIndicator);
console.error('Ошибка сетевого запроса к IO Intelligence:', error);
reject(new Error(`Ошибка сетевого запроса: ${error.statusText || 'Сетевая ошибка'}`));
}
});
});
}