Convert page-agent to pure ASCII to avoid mojibake
This commit is contained in:
+35
-35
@@ -1,8 +1,8 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Page Agent — LLM in the page
|
// @name Page Agent - LLM in the page
|
||||||
// @namespace https://github.com/example/page-agent
|
// @namespace https://github.com/example/page-agent
|
||||||
// @version 0.1.0
|
// @version 0.1.0
|
||||||
// @description Turn any web page into an LLM agent that can run JS in the page to inspect, extract, fetch and act — then bake what it learned into a standalone, LLM-free userscript.
|
// @description Turn any web page into an LLM agent that can run JS in the page to inspect, extract, fetch and act - then bake what it learned into a standalone, LLM-free userscript.
|
||||||
// @match *://*/*
|
// @match *://*/*
|
||||||
// @run-at document-idle
|
// @run-at document-idle
|
||||||
// @grant GM.xmlHttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
@@ -41,12 +41,12 @@
|
|||||||
const maxSteps = 8; // upper bound on the agent's tool-use loop
|
const maxSteps = 8; // upper bound on the agent's tool-use loop
|
||||||
|
|
||||||
// The panel starts fully hidden. Reveal it with:
|
// The panel starts fully hidden. Reveal it with:
|
||||||
// Desktop — Ctrl+Shift+Space (see HOTKEY)
|
// Desktop - Ctrl+Shift+Space (see HOTKEY)
|
||||||
// Mobile — three-finger tap anywhere on the page
|
// Mobile - three-finger tap anywhere on the page
|
||||||
const HOTKEY = { ctrl: true, shift: true, alt: false, code: 'Space', label: 'Ctrl+Shift+Space' };
|
const HOTKEY = { ctrl: true, shift: true, alt: false, code: 'Space', label: 'Ctrl+Shift+Space' };
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// safeStringify — serialize arbitrary values without blowing up
|
// safeStringify - serialize arbitrary values without blowing up
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function safeStringify(value, maxLen = 20000) {
|
function safeStringify(value, maxLen = 20000) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
if (out === undefined) out = String(value); // undefined coercion
|
if (out === undefined) out = String(value); // undefined coercion
|
||||||
if (out.length > maxLen) {
|
if (out.length > maxLen) {
|
||||||
const total = out.length;
|
const total = out.length;
|
||||||
out = out.slice(0, maxLen) + '…[truncated, ' + total + ' chars]';
|
out = out.slice(0, maxLen) + '...[truncated, ' + total + ' chars]';
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
'Execute JavaScript in the context of the current web page. Your code runs ' +
|
'Execute JavaScript in the context of the current web page. Your code runs ' +
|
||||||
'as the BODY of an async function, so you can use `await` and you MUST `return` ' +
|
'as the BODY of an async function, so you can use `await` and you MUST `return` ' +
|
||||||
'a value to see any result. The returned value is JSON-stringified and sent back ' +
|
'a value to see any result. The returned value is JSON-stringified and sent back ' +
|
||||||
'to you, so keep it SMALL and RELEVANT — return only the specific fields/text you ' +
|
'to you, so keep it SMALL and RELEVANT - return only the specific fields/text you ' +
|
||||||
'need, never whole DOM trees or huge arrays. Use it to query selectors, extract ' +
|
'need, never whole DOM trees or huge arrays. Use it to query selectors, extract ' +
|
||||||
'content, call fetch(), read/modify the page, click, fill and navigate. Take ' +
|
'content, call fetch(), read/modify the page, click, fill and navigate. Take ' +
|
||||||
'several small targeted steps rather than one giant snippet.',
|
'several small targeted steps rather than one giant snippet.',
|
||||||
@@ -190,9 +190,9 @@
|
|||||||
args = { code: '' };
|
args = { code: '' };
|
||||||
}
|
}
|
||||||
const code = args.code || '';
|
const code = args.code || '';
|
||||||
log('» run_js:\n' + code);
|
log('>> run_js:\n' + code);
|
||||||
const result = await runJs(code);
|
const result = await runJs(code);
|
||||||
log('« result: ' + result.slice(0, 500));
|
log('<< result: ' + result.slice(0, 500));
|
||||||
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// UI — floating panel (bottom-right, dark), hidden by default
|
// UI - floating panel (bottom-right, dark), hidden by default
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const PANEL_ID = 'llm-page-agent-panel';
|
const PANEL_ID = 'llm-page-agent-panel';
|
||||||
if (document.getElementById(PANEL_ID)) return; // avoid double-inject
|
if (document.getElementById(PANEL_ID)) return; // avoid double-inject
|
||||||
@@ -254,15 +254,15 @@
|
|||||||
panel.className = 'pa-hidden'; // hidden until revealed
|
panel.className = 'pa-hidden'; // hidden until revealed
|
||||||
panel.innerHTML =
|
panel.innerHTML =
|
||||||
'<div class="pa-header">' +
|
'<div class="pa-header">' +
|
||||||
'<span class="pa-title">🤖 Page Agent</span>' +
|
'<span class="pa-title">[AI] Page Agent</span>' +
|
||||||
'<button class="pa-iconbtn" data-act="settings" title="Settings">⚙</button>' +
|
'<button class="pa-iconbtn" data-act="settings" title="Settings">[cfg]</button>' +
|
||||||
'<button class="pa-iconbtn" data-act="collapse" title="Collapse">—</button>' +
|
'<button class="pa-iconbtn" data-act="collapse" title="Collapse">-</button>' +
|
||||||
'<button class="pa-iconbtn" data-act="hide" title="Hide (reopen with ' + HOTKEY.label + ' or a three-finger tap)">✕</button>' +
|
'<button class="pa-iconbtn" data-act="hide" title="Hide (reopen with ' + HOTKEY.label + ' or a three-finger tap)">x</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="pa-body">' +
|
'<div class="pa-body">' +
|
||||||
'<div class="pa-log"></div>' +
|
'<div class="pa-log"></div>' +
|
||||||
'<div class="pa-row">' +
|
'<div class="pa-row">' +
|
||||||
'<input class="pa-input" type="text" placeholder="Ask the agent to do something…" />' +
|
'<input class="pa-input" type="text" placeholder="Ask the agent to do something..." />' +
|
||||||
'<button class="pa-btn" data-act="send">Send</button>' +
|
'<button class="pa-btn" data-act="send">Send</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="pa-row">' +
|
'<div class="pa-row">' +
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Visibility — hidden by default; toggle via hotkey / gesture
|
// Visibility - hidden by default; toggle via hotkey / gesture
|
||||||
// ============================================================
|
// ============================================================
|
||||||
let hintShown = false;
|
let hintShown = false;
|
||||||
function isHidden() { return panel.classList.contains('pa-hidden'); }
|
function isHidden() { return panel.classList.contains('pa-hidden'); }
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
sendBtn.dataset.label = sendBtn.textContent;
|
sendBtn.dataset.label = sendBtn.textContent;
|
||||||
sendBtn.innerHTML = '<span class="pa-spinner"></span>';
|
sendBtn.innerHTML = '<span class="pa-spinner"></span>';
|
||||||
statusEl = document.createElement('div');
|
statusEl = document.createElement('div');
|
||||||
statusEl.innerHTML = '<span class="pa-spinner"></span> ' + (label || 'thinking…');
|
statusEl.innerHTML = '<span class="pa-spinner"></span> ' + (label || 'thinking...');
|
||||||
logEl.appendChild(statusEl);
|
logEl.appendChild(statusEl);
|
||||||
logEl.scrollTop = logEl.scrollHeight;
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
} else {
|
} else {
|
||||||
@@ -353,13 +353,13 @@
|
|||||||
|
|
||||||
async function run(request) {
|
async function run(request) {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
if (!CONFIG.apiKey) { append('⚠ No API key set. Click ⚙ to configure.'); return; }
|
if (!CONFIG.apiKey) { append('! No API key set. Click [cfg] to configure.'); return; }
|
||||||
append('› ' + request);
|
append('> ' + request);
|
||||||
setBusy(true, 'thinking…');
|
setBusy(true, 'thinking...');
|
||||||
try {
|
try {
|
||||||
const { answer, messages } = await agent(request, append, conversation);
|
const { answer, messages } = await agent(request, append, conversation);
|
||||||
conversation = messages; // persist for later turns
|
conversation = messages; // persist for later turns
|
||||||
append('🤖 ' + answer);
|
append('[AI] ' + answer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Convert / Download — bake the session into a standalone userscript
|
// Convert / Download - bake the session into a standalone userscript
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const CONVERT_PROMPT =
|
const CONVERT_PROMPT =
|
||||||
'Now convert EVERYTHING you learned and did in this session into a single, ' +
|
'Now convert EVERYTHING you learned and did in this session into a single, ' +
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
'Include a complete ==UserScript== metadata header (@name, @namespace, @version, ' +
|
'Include a complete ==UserScript== metadata header (@name, @namespace, @version, ' +
|
||||||
'@description, an appropriate @match, @run-at and any @grant/@connect you need). ' +
|
'@description, an appropriate @match, @run-at and any @grant/@connect you need). ' +
|
||||||
'The script must be self-contained and idempotent. Output ONLY the finished ' +
|
'The script must be self-contained and idempotent. Output ONLY the finished ' +
|
||||||
'userscript inside a single ' + BT3 + 'javascript code block — no explanation ' +
|
'userscript inside a single ' + BT3 + 'javascript code block - no explanation ' +
|
||||||
'before or after.';
|
'before or after.';
|
||||||
|
|
||||||
function extractCode(text) {
|
function extractCode(text) {
|
||||||
@@ -436,12 +436,12 @@
|
|||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
|
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||||
setBusy(true, 'converting…');
|
setBusy(true, 'converting...');
|
||||||
try {
|
try {
|
||||||
const code = await generateUserscript();
|
const code = await generateUserscript();
|
||||||
await copyToClipboard(code);
|
await copyToClipboard(code);
|
||||||
append('✅ Userscript generated & copied to clipboard (' + code.length + ' chars). Use “Download .user.js” to save it.');
|
append('OK Userscript generated & copied to clipboard (' + code.length + ' chars). Use "Download .user.js" to save it.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -451,12 +451,12 @@
|
|||||||
|
|
||||||
async function download() {
|
async function download() {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
|
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||||
setBusy(true, 'preparing download…');
|
setBusy(true, 'preparing download...');
|
||||||
try {
|
try {
|
||||||
const code = lastCode || await generateUserscript();
|
const code = lastCode || await generateUserscript();
|
||||||
saveFile(code, 'page-agent-generated.user.js');
|
saveFile(code, 'page-agent-generated.user.js');
|
||||||
append('⬇ Saved page-agent-generated.user.js (' + code.length + ' chars).');
|
append('v Saved page-agent-generated.user.js (' + code.length + ' chars).');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -474,7 +474,7 @@
|
|||||||
if (key !== null) { CONFIG.apiKey = key.trim(); await GM.setValue('apiKey', CONFIG.apiKey); }
|
if (key !== null) { CONFIG.apiKey = key.trim(); await GM.setValue('apiKey', CONFIG.apiKey); }
|
||||||
const mdl = prompt('Model:', CONFIG.model);
|
const mdl = prompt('Model:', CONFIG.model);
|
||||||
if (mdl !== null) { CONFIG.model = mdl.trim() || DEFAULTS.model; await GM.setValue('model', CONFIG.model); }
|
if (mdl !== null) { CONFIG.model = mdl.trim() || DEFAULTS.model; await GM.setValue('model', CONFIG.model); }
|
||||||
append('⚙ Settings saved — endpoint=' + CONFIG.endpoint + ' model=' + CONFIG.model + ' key=' + (CONFIG.apiKey ? '(set)' : '(empty)'));
|
append('[cfg] Settings saved - endpoint=' + CONFIG.endpoint + ' model=' + CONFIG.model + ' key=' + (CONFIG.apiKey ? '(set)' : '(empty)'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -488,11 +488,11 @@
|
|||||||
hideBtn.addEventListener('click', hidePanel);
|
hideBtn.addEventListener('click', hidePanel);
|
||||||
collapseBtn.addEventListener('click', () => {
|
collapseBtn.addEventListener('click', () => {
|
||||||
const collapsed = panel.classList.toggle('pa-collapsed');
|
const collapsed = panel.classList.toggle('pa-collapsed');
|
||||||
collapseBtn.textContent = collapsed ? '▢' : '—';
|
collapseBtn.textContent = collapsed ? '[ ]' : '-';
|
||||||
collapseBtn.title = collapsed ? 'Expand' : 'Collapse';
|
collapseBtn.title = collapsed ? 'Expand' : 'Collapse';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Desktop reveal/hide — global hotkey (Ctrl+Shift+Space by default).
|
// Desktop reveal/hide - global hotkey (Ctrl+Shift+Space by default).
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (!!e.ctrlKey === HOTKEY.ctrl && !!e.shiftKey === HOTKEY.shift &&
|
if (!!e.ctrlKey === HOTKEY.ctrl && !!e.shiftKey === HOTKEY.shift &&
|
||||||
!!e.altKey === HOTKEY.alt && e.code === HOTKEY.code) {
|
!!e.altKey === HOTKEY.alt && e.code === HOTKEY.code) {
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Mobile reveal/hide — three-finger tap anywhere on the page.
|
// Mobile reveal/hide - three-finger tap anywhere on the page.
|
||||||
window.addEventListener('touchstart', (e) => {
|
window.addEventListener('touchstart', (e) => {
|
||||||
if (e.touches && e.touches.length === 3) {
|
if (e.touches && e.touches.length === 3) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -511,6 +511,6 @@
|
|||||||
}
|
}
|
||||||
}, { capture: true, passive: false });
|
}, { capture: true, passive: false });
|
||||||
|
|
||||||
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via ⚙ first. ') + 'Ask me to inspect or act on this page.');
|
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via [cfg] first. ') + 'Ask me to inspect or act on this page.');
|
||||||
showToast('🤖 Page Agent loaded — ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
|
showToast('[AI] Page Agent loaded - ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user