// ==UserScript==
// @name עוזר AI לכל דף
// @namespace ai-page-assistant
// @version 1.3
// @description כפתור AI בכל דף: מעתיק את כל הטקסט שבמסך ועונה לפי מה שאתה שואל
// @match *://*/*
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @connect generativelanguage.googleapis.com
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const DEFAULT_MODEL = 'gemini-flash-lite-latest';
const KEY_URL = 'https://aistudio.google.com/apikey';
const MAX_CHARS = 150000;
const getKey = () => GM_getValue('api_key', '');
const getModel = () => GM_getValue('model', DEFAULT_MODEL);
function askKey(force) {
let key = getKey();
if (!key || force) {
if (!key) { try { GM_openInTab(KEY_URL, { active: false }); } catch (e) {} }
const input = prompt('הכנס מפתח API של Gemini (נשמר רק בדפדפן שלך).\nלהשגת מפתח חינם: ' + KEY_URL, key || '');
if (input === null) return key;
key = input.trim();
GM_setValue('api_key', key);
}
return key;
}
function getPageText() {
let text = (document.body && document.body.innerText) || '';
text = text.replace(/\n{3,}/g, '\n\n').trim();
if (text.length > MAX_CHARS) text = text.slice(0, MAX_CHARS) + '\n...[נחתך]';
return text;
}
// ---------- UI ----------
const host = document.createElement('div');
host.style.cssText = 'all:initial;position:fixed;z-index:2147483647;';
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `
עוזר AIעונה לפי הדף שאתה צופה בו
לחץ על אחד הכפתורים למטה
או שאל שאלה על הדף
`;
document.documentElement.appendChild(host);
const $ = (id) => root.getElementById(id);
const panel = $('panel'), msgs = $('msgs'), q = $('q'), send = $('send'), info = $('info'),
fab = $('fab'), empty = $('empty');
let pageText = '';
let history = [];
let gen = 0;
function loadPage() {
pageText = getPageText();
info.textContent = 'הועתקו ' + pageText.length.toLocaleString() + ' תווים מהדף';
}
function scrollDown() { msgs.scrollTop = msgs.scrollHeight; }
function addMsg(text, cls) {
empty.classList.add('hide');
const d = document.createElement('div');
d.className = 'm ' + cls;
d.textContent = text;
msgs.appendChild(d);
scrollDown();
return d;
}
function addTyping() {
empty.classList.add('hide');
const d = document.createElement('div');
d.className = 'm a dots';
d.innerHTML = '';
msgs.appendChild(d);
scrollDown();
return d;
}
function reveal(el, text) {
const n = text.length;
const dur = Math.min(1500, Math.max(250, n * 7));
const t0 = performance.now();
(function step(now) {
const k = Math.min(n, Math.floor(((now - t0) / dur) * n));
el.firstChild.textContent = text.slice(0, k);
scrollDown();
if (k < n) requestAnimationFrame(step); else el.firstChild.textContent = text;
})(t0);
}
function copyText(t, btn) {
const done = () => { btn.textContent = 'הועתק ✓'; setTimeout(() => (btn.textContent = 'העתק'), 1200); };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(t).then(done, () => {});
else {
const ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta);
ta.select(); try { document.execCommand('copy'); done(); } catch (e) {} ta.remove();
}
}
function showAnswer(slot, text) {
slot.className = 'm a';
slot.innerHTML = '';
slot.lastChild.onclick = () => copyText(text, slot.lastChild);
reveal(slot, text);
}
function openPanel() {
if (!askKey(false)) return;
loadPage();
panel.classList.add('open');
fab.classList.add('on');
setTimeout(() => q.focus(), 300);
}
function closePanel() {
panel.classList.remove('open');
fab.classList.remove('on');
}
function toggle() { panel.classList.contains('open') ? closePanel() : openPanel(); }
function clearChat() {
gen++;
history = [];
msgs.innerHTML = '';
empty.classList.remove('hide');
send.disabled = false;
}
fab.onclick = toggle;
$('close').onclick = closePanel;
$('refresh').onclick = () => { loadPage(); addMsg('הטקסט של הדף נקרא מחדש.', 'a'); };
$('clear').onclick = clearChat;
$('settings').onclick = () => askKey(true);
root.querySelectorAll('.chip').forEach((c) => (c.onclick = () => submit(c.dataset.p)));
GM_registerMenuCommand('פתח/סגור עוזר AI', toggle);
GM_registerMenuCommand('שנה מפתח API', () => askKey(true));
GM_registerMenuCommand('שנה מודל', () => {
const m = prompt('שם מודל Gemini:', getModel());
if (m) GM_setValue('model', m.trim());
});
// ---------- AI ----------
function callGemini(userText) {
return new Promise((resolve, reject) => {
const key = getKey();
if (!key) return reject(new Error('אין מפתח API'));
history.push({ role: 'user', parts: [{ text: userText }] });
const body = {
systemInstruction: {
parts: [{
text:
'אתה עוזר שעונה למשתמש על סמך תוכן הדף שהוא צופה בו. ענה בשפה שבה המשתמש כותב (ברירת מחדל: עברית), בקצרה וישירות.\n' +
'כתובת הדף: ' + location.href + '\nכותרת: ' + document.title +
'\n\n===== תוכן הדף =====\n' + pageText
}]
},
contents: history
};
GM_xmlhttpRequest({
method: 'POST',
url: 'https://generativelanguage.googleapis.com/v1beta/models/' +
encodeURIComponent(getModel()) + ':generateContent',
headers: { 'Content-Type': 'application/json', 'x-goog-api-key': key },
data: JSON.stringify(body),
timeout: 120000,
onload: (r) => {
try {
const j = JSON.parse(r.responseText);
if (r.status >= 300) throw new Error((j.error && j.error.message) || ('שגיאה ' + r.status));
const t = (j.candidates?.[0]?.content?.parts || []).map(p => p.text || '').join('');
if (!t) throw new Error('תשובה ריקה');
history.push({ role: 'model', parts: [{ text: t }] });
resolve(t);
} catch (e) { history.pop(); reject(e); }
},
onerror: () => { history.pop(); reject(new Error('שגיאת רשת')); },
ontimeout: () => { history.pop(); reject(new Error('הבקשה נתקעה')); }
});
});
}
async function submit(textArg) {
const text = (typeof textArg === 'string' ? textArg : q.value).trim();
if (!text || send.disabled) return;
if (typeof textArg !== 'string') q.value = '';
addMsg(text, 'u');
const wait = addTyping();
const g = gen;
send.disabled = true;
try {
const ans = await callGemini(text);
if (g !== gen) return;
showAnswer(wait, ans);
} catch (e) {
if (g !== gen) return;
wait.className = 'm e';
wait.textContent = 'שגיאה: ' + e.message;
}
send.disabled = false;
scrollDown();
q.focus();
}
send.onclick = () => submit();
q.addEventListener('keydown', (e) => {
e.stopPropagation();
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
if (e.key === 'Escape') closePanel();
});
q.addEventListener('keyup', (e) => e.stopPropagation());
q.addEventListener('keypress', (e) => e.stopPropagation());
})();