AI studio
// ==UserScript==
// @name AI Giải Bài Tập
// @namespace http://tampermonkey.net/
// @version 1.0
// @description AI studio
// @author Tran Bao Ngoc
// @match https://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect generativelanguage.googleapis.com
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// ==/UserScript==
(async function() {
'use strict';
let GEMINI_API_KEY = GM_getValue('geminiApiKey', "");
// === UI Tối Giản Nhỏ Gọn ===
const ui = document.createElement('div');
ui.id = 'aiPanel';
ui.innerHTML = `
<div class="ai-header">
<h2>Trần Bảo Ngọc</h2>
<div id="aiStatus">Ready</div>
</div>
<div id="apiKeySection">
<label>API Key Gemini</label>
<input type="password" id="apiKeyInput" value="${GEMINI_API_KEY}" placeholder="Nhập API key của bạn..." />
</div>
<button id="changeApiBtn" style="display:none; width: 100%; margin-bottom: 8px;">Thay đổi Key</button>
<div class="ai-selects">
<select id="modelSelect">
<option value="gemini-flash-latest">⚡️ Flash</option>
<option value="gemini-2.5-pro">✨ Pro 2.5</option>
</select>
<select id="lang"><option value="vi">VI</option><option value="en">EN</option></select>
<select id="subject">
<option>Toán</option><option>Lý</option><option>Hóa</option><option>Sinh</option><option>Sử</option><option>Địa</option><option>Văn</option><option>Anh</option><option>GDCD</option><option>Tin học</option>
</select>
</div>
<div class="ai-selects">
<select id="outputMode" style="width:100%">
<option value="answer">Chỉ đáp án</option>
<option value="explain">Giải thích chi tiết</option>
<option value="custom">Tùy chỉnh...</option>
</select>
</div>
<div id="customPromptSection" style="display:none; margin-bottom: 8px;">
<label>Yêu cầu tùy chỉnh</label>
<textarea id="customPromptInput" rows="3" placeholder="Ví dụ: Tóm tắt nội dung trong ảnh..."></textarea>
</div>
<div class="ai-actions">
<button id="btnShot" disabled>📸 Kéo vùng</button>
<button id="btnFullPage" disabled>📄 Toàn trang</button>
</div>
<button id="btnToggleTextMode" class="text-mode-btn" disabled>📝 Nhập câu hỏi</button>
<div id="textInputSection" style="display: none; margin-top: 8px;">
<label>Nhập câu hỏi của bạn vào đây</label>
<textarea id="textQuestionInput" rows="4" placeholder="Ví dụ: Trình bày vai trò của quang hợp..."></textarea>
<button id="btnSendTextQuestion" style="width:100%; margin-top: 4px;">Gửi câu hỏi</button>
</div>
<div class="ai-box">
<label>Ảnh</label>
<div id="imgBox"></div>
</div>
<div class="ai-box">
<label>Đáp án</label>
<div id="ansBox"></div>
</div>
`;
document.body.appendChild(ui);
// === Lấy các phần tử DOM ===
const apiKeyInput = document.getElementById('apiKeyInput');
const apiKeySection = document.getElementById('apiKeySection');
const changeApiBtn = document.getElementById('changeApiBtn');
const aiStatus = document.getElementById('aiStatus');
const btnShot = document.getElementById('btnShot');
const btnFullPage = document.getElementById('btnFullPage');
const btnToggleTextMode = document.getElementById('btnToggleTextMode');
const textInputSection = document.getElementById('textInputSection');
const textQuestionInput = document.getElementById('textQuestionInput');
const btnSendTextQuestion = document.getElementById('btnSendTextQuestion');
const outputModeSelect = document.getElementById('outputMode');
const customPromptSection = document.getElementById('customPromptSection');
const customPromptInput = document.getElementById('customPromptInput');
const allActionButtons = [btnShot, btnFullPage, btnToggleTextMode];
// === Hàm Gửi Yêu Cầu Đến Gemini (Đã cập nhật) ===
function sendToGemini(prompt, base64Image = null) {
const model = document.getElementById('modelSelect').value;
const ansBox = document.getElementById('ansBox');
const imgBox = document.getElementById('imgBox');
ansBox.innerHTML = "⏳ Đang gửi đến Gemini...";
ansBox.classList.add('loading');
let parts = [{ text: prompt }];
if (base64Image) {
parts.push({ inlineData: { mimeType: "image/jpeg", data: base64Image } });
}
GM_xmlhttpRequest({
method: "POST",
url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GEMINI_API_KEY}`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({
contents: [{ parts: parts }],
generationConfig: { "temperature": 0.2, "topP": 0.95, "topK": 40 }
}),
onload: r => {
ansBox.classList.remove('loading');
try {
const data = JSON.parse(r.responseText);
if (data.error) throw new Error(data.error.message);
const result = data?.candidates?.[0]?.content?.parts?.[0]?.text || "❌ Không nhận được phản hồi.";
// *** THAY ĐỔI: Ẩn ảnh khi nhận được câu trả lời ***
if (base64Image) {
imgBox.innerHTML = '';
}
typeEffect(ansBox, result.trim());
} catch (err) {
ansBox.innerHTML = `<b style="color:red;">Lỗi API:</b> ${err.message || "Kiểm tra F12 > Console để biết thêm chi tiết"}`;
console.error("Lỗi Gemini:", r.responseText);
}
},
onerror: err => {
ansBox.classList.remove('loading');
ansBox.innerHTML = `<b style="color:red;">Lỗi request:</b> ${JSON.stringify(err)}`;
}
});
}
// === Hàm tạo Prompt ===
function createPrompt(isImage = true) {
const subj = document.getElementById('subject').value;
const lang = document.getElementById('lang').value;
const mode = document.getElementById('outputMode').value;
const langStr = lang === 'vi' ? 'Tiếng Việt' : 'English';
const source = isImage ? 'trong ảnh' : 'được cung cấp';
if (mode === 'custom') {
const customText = customPromptInput.value.trim();
if (!customText) {
document.getElementById('ansBox').innerHTML = '<b style="color:red;">Lỗi:</b> Vui lòng nhập yêu cầu tùy chỉnh của bạn.';
return null;
}
return `${customText} (Trả lời bằng ${langStr})`;
} else if (mode === 'answer') {
return `Với bài tập môn ${subj} ${source}, chỉ đưa ra đáp án cuối cùng. Không giải thích. Không dùng markdown. Trả lời bằng ${langStr}.`;
} else { // 'explain'
return `Phân tích và giải chi tiết bài tập môn ${subj} ${source}. Suy nghĩ từng bước, đưa ra công thức và lời giải rõ ràng. Trả lời bằng ${langStr}.`;
}
}
// === Hàm kiểm tra API Key ===
function checkApiKey(key) {
if (!key) {
aiStatus.textContent = 'Vui lòng nhập API Key';
aiStatus.style.color = '#e74c3c';
allActionButtons.forEach(b => b.disabled = true);
apiKeySection.style.display = 'block';
changeApiBtn.style.display = 'none';
return;
}
aiStatus.textContent = '🔄 Đang kiểm tra key...';
aiStatus.style.color = '#f1c40f';
allActionButtons.forEach(b => b.disabled = true);
GM_xmlhttpRequest({
method: "POST",
url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent?key=${key}`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }] }),
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (data.candidates) {
aiStatus.textContent = '✅ Key hợp lệ';
aiStatus.style.color = '#00b894';
GEMINI_API_KEY = key;
GM_setValue('geminiApiKey', key);
apiKeySection.style.display = 'none';
changeApiBtn.style.display = 'block';
allActionButtons.forEach(b => b.disabled = false);
} else { throw new Error("Invalid response"); }
} catch (e) {
aiStatus.textContent = '❌ Key không hợp lệ.';
aiStatus.style.color = '#e74c3c';
allActionButtons.forEach(b => b.disabled = true);
apiKeySection.style.display = 'block';
changeApiBtn.style.display = 'none';
}
} else {
aiStatus.textContent = '❌ Key không hợp lệ/hết hạn';
aiStatus.style.color = '#e74c3c';
allActionButtons.forEach(b => b.disabled = true);
apiKeySection.style.display = 'block';
changeApiBtn.style.display = 'none';
}
},
onerror: function(error) {
aiStatus.textContent = ' Lỗi mạng khi kiểm tra key';
aiStatus.style.color = '#e74c3c';
allActionButtons.forEach(b => b.disabled = true);
}
});
}
// === Hàm xử lý chụp ảnh ===
async function handleScreenshot(options = {}) {
const imgBox = document.getElementById('imgBox');
const ansBox = document.getElementById('ansBox');
imgBox.innerHTML = "🕐 Đang chụp ảnh...";
ansBox.innerHTML = "";
try {
// Luôn cuộn lên đầu trang để đảm bảo chụp từ đầu khi chụp toàn trang
if (!options.x && !options.y) {
window.scrollTo(0, 0);
}
const canvas = await html2canvas(document.body, {
...options,
scale: 1.5,
useCORS: true,
allowTaint: true
});
const base64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1];
imgBox.innerHTML = `<img src="${canvas.toDataURL()}">`;
const prompt = createPrompt(true);
if (prompt) {
sendToGemini(prompt, base64);
}
} catch (err) {
imgBox.innerHTML = `<b style="color:red;">❌ Lỗi chụp ảnh:</b> ${err.message}`;
ansBox.innerHTML = '';
}
}
// === Xử lý sự kiện ===
apiKeyInput.addEventListener('blur', () => checkApiKey(apiKeyInput.value.trim()));
changeApiBtn.addEventListener('click', () => {
apiKeySection.style.display = 'block';
changeApiBtn.style.display = 'none';
apiKeyInput.focus();
allActionButtons.forEach(b => b.disabled = true);
aiStatus.textContent = "Nhập key mới và click ra ngoài";
aiStatus.style.color = "#f1c40f";
});
outputModeSelect.addEventListener('change', () => {
customPromptSection.style.display = (outputModeSelect.value === 'custom') ? 'block' : 'none';
});
btnToggleTextMode.addEventListener('click', () => {
const isVisible = textInputSection.style.display === 'block';
textInputSection.style.display = isVisible ? 'none' : 'block';
});
btnSendTextQuestion.addEventListener('click', () => {
const question = textQuestionInput.value.trim();
if (!question) {
document.getElementById('ansBox').innerHTML = '<b style="color:red;">Lỗi:</b> Vui lòng nhập câu hỏi.';
return;
}
const prompt = createPrompt(false);
if (prompt) {
const fullPrompt = `Câu hỏi: "${question}".\n\n${prompt}`;
document.getElementById('imgBox').innerHTML = "";
sendToGemini(fullPrompt, null);
}
});
// === CSS ===
GM_addStyle(`
#aiPanel {
position: fixed; top: 30px; left: 30px; width: 280px; background: #1e1e1e;
color: #fff; z-index: 999999; padding: 12px; border-radius: 8px; font-family: 'Segoe UI', sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.3); display: none; cursor: move; font-size: 12px; border: 1px solid #333;
}
.ai-header { text-align: center; margin-bottom: 12px; border-bottom: 1px solid #333; padding-bottom: 8px; }
.ai-header h2 { margin: 0 0 4px; font-size: 14px; color: #00b894; }
#aiStatus { font-size: 10px; color: #aaa; min-height: 12px; }
#apiKeySection, .ai-selects, .ai-box, #customPromptSection, .ai-actions { margin-bottom: 8px; }
label { display: block; font-size: 11px; color: #ccc; margin-bottom: 4px; }
#apiKeyInput, textarea {
width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #444; border-radius: 4px; background: #2a2a2a; color: #fff; font-size: 11px;
}
#apiKeyInput:focus, textarea:focus { outline: none; border-color: #00b894; }
textarea { resize: vertical; }
.ai-selects, .ai-actions { display: flex; gap: 4px; }
select, button { flex: 1; padding: 8px; border: 1px solid #444; border-radius: 4px; background: #2a2a2a; color: #fff; font-size: 11px; }
select:focus, button:focus { outline: none; border-color: #00b894; }
button { cursor: pointer; border: none; transition: background 0.2s ease; }
button:disabled { background: #555 !important; cursor: not-allowed; opacity: 0.6; }
.ai-actions button { background: #00b894; }
.ai-actions button:hover:not(:disabled) { background: #009975; }
#btnToggleTextMode { width: 100%; background: #e67e22; margin-bottom: 4px; }
#btnToggleTextMode:hover:not(:disabled) { background: #d35400; }
#btnSendTextQuestion { background: #3498db; }
#btnSendTextQuestion:hover:not(:disabled) { background: #2980b9; }
#changeApiBtn { background: #3498db; }
#changeApiBtn:hover { background: #2980b9; }
.ai-box div { min-height: 40px; background: #2a2a2a; padding: 8px; border-radius: 4px; font-size: 11px; white-space: pre-wrap; word-wrap: break-word; border: 1px solid #444; }
#imgBox img { max-width: 100%; border-radius: 4px; }
#ansBox.loading::after { content: '⏳'; display: inline-block; animation: spin 1s linear infinite; margin-left: 5px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#aiSnipOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2147483647; display: none; cursor: crosshair; }
#aiSnipBox { position: absolute; border: 2px dashed #00b894; background: rgba(0,184,148,0.1); z-index: 2147483648; display: none; pointer-events: none; border-radius: 4px; }
`);
// === Kéo thả panel ===
let dragging = false, dragOffset = {x:0,y:0};
ui.addEventListener('mousedown', e => {
if (!['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA'].includes(e.target.tagName)) {
dragging = true;
dragOffset.x = e.clientX - ui.offsetLeft;
dragOffset.y = e.clientY - ui.offsetTop;
}
});
document.addEventListener('mousemove', e => { if (dragging) { ui.style.left = (e.clientX - dragOffset.x) + 'px'; ui.style.top = (e.clientY - dragOffset.y) + 'px'; } });
document.addEventListener('mouseup', () => { dragging = false; });
// === Toggle bằng ShiftRight ===
document.addEventListener('keydown', e => {
if (e.code === 'ShiftRight') {
ui.style.display = ui.style.display === 'none' ? 'block' : 'none';
if(ui.style.display === 'block') { checkApiKey(GM_getValue('geminiApiKey', "")); }
}
});
// === Overlay và Chụp ảnh ===
const overlay = document.createElement('div'); overlay.id = 'aiSnipOverlay'; document.body.appendChild(overlay);
const snipBox = document.createElement('div'); snipBox.id = 'aiSnipBox'; document.body.appendChild(snipBox);
let selecting = false, startX, startY, endX, endY;
btnShot.onclick = () => {
selecting = true;
overlay.style.display = 'block';
ui.style.display = 'none';
};
// --- CHỤP TOÀN BỘ TRANG ---
btnFullPage.onclick = () => {
ui.style.display = 'none'; // Ẩn panel trước khi chụp
setTimeout(() => {
handleScreenshot({}).finally(() => {
ui.style.display = 'block'; // Hiện lại panel sau khi xong
});
}, 150);
};
overlay.addEventListener('mousedown', e => {
if (!selecting) return;
startX = e.clientX; startY = e.clientY;
snipBox.style.left = startX + 'px'; snipBox.style.top = startY + 'px';
snipBox.style.width = '0px'; snipBox.style.height = '0px';
snipBox.style.display = 'block';
});
overlay.addEventListener('mousemove', e => {
if (!selecting || startX === undefined) return;
endX = e.clientX; endY = e.clientY;
snipBox.style.left = Math.min(startX, endX) + 'px';
snipBox.style.top = Math.min(startY, endY) + 'px';
snipBox.style.width = Math.abs(endX - startX) + 'px';
snipBox.style.height = Math.abs(endY - startY) + 'px';
});
overlay.addEventListener('mouseup', async e => {
if (!selecting || startX === undefined) return;
const left = Math.min(startX, endX);
const top = Math.min(startY, endY);
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
selecting = false;
overlay.style.display = 'none';
snipBox.style.display = 'none';
ui.style.display = 'block';
startX = startY = endX = endY = undefined;
if (width < 10 || height < 10) return;
handleScreenshot({ x: left, y: top, width: width, height: height });
});
// === Hiệu ứng gõ chữ ===
function typeEffect(el, text, speed = 10) {
el.innerHTML = "";
let i = 0;
function typing() {
if (i < text.length) {
el.innerHTML += text.charAt(i++);
setTimeout(typing, speed);
}
};
typing();
}
// === Khởi chạy lần đầu ===
checkApiKey(GEMINI_API_KEY);
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址