// ==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 = `
לחץ על אחד הכפתורים למטה
או שאל שאלה על הדף
`; 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()); })();