Convert page-agent to pure ASCII to avoid mojibake

This commit is contained in:
2026-07-08 12:19:25 -04:00
parent 587a3f7aab
commit a921bad7ef
+35 -35
View File
@@ -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.');
})(); })();