Convert page-agent to pure ASCII to avoid mojibake
This commit is contained in:
+35
-35
@@ -1,8 +1,8 @@
|
||||
// ==UserScript==
|
||||
// @name Page Agent — LLM in the page
|
||||
// @name Page Agent - LLM in the page
|
||||
// @namespace https://github.com/example/page-agent
|
||||
// @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 *://*/*
|
||||
// @run-at document-idle
|
||||
// @grant GM.xmlHttpRequest
|
||||
@@ -41,12 +41,12 @@
|
||||
const maxSteps = 8; // upper bound on the agent's tool-use loop
|
||||
|
||||
// The panel starts fully hidden. Reveal it with:
|
||||
// Desktop — Ctrl+Shift+Space (see HOTKEY)
|
||||
// Mobile — three-finger tap anywhere on the page
|
||||
// Desktop - Ctrl+Shift+Space (see HOTKEY)
|
||||
// Mobile - three-finger tap anywhere on the page
|
||||
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) {
|
||||
const seen = new WeakSet();
|
||||
@@ -70,7 +70,7 @@
|
||||
if (out === undefined) out = String(value); // undefined coercion
|
||||
if (out.length > maxLen) {
|
||||
const total = out.length;
|
||||
out = out.slice(0, maxLen) + '…[truncated, ' + total + ' chars]';
|
||||
out = out.slice(0, maxLen) + '...[truncated, ' + total + ' chars]';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -86,7 +86,7 @@
|
||||
'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` ' +
|
||||
'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 ' +
|
||||
'content, call fetch(), read/modify the page, click, fill and navigate. Take ' +
|
||||
'several small targeted steps rather than one giant snippet.',
|
||||
@@ -190,9 +190,9 @@
|
||||
args = { code: '' };
|
||||
}
|
||||
const code = args.code || '';
|
||||
log('» run_js:\n' + code);
|
||||
log('>> run_js:\n' + 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 });
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
if (document.getElementById(PANEL_ID)) return; // avoid double-inject
|
||||
@@ -254,15 +254,15 @@
|
||||
panel.className = 'pa-hidden'; // hidden until revealed
|
||||
panel.innerHTML =
|
||||
'<div class="pa-header">' +
|
||||
'<span class="pa-title">🤖 Page Agent</span>' +
|
||||
'<button class="pa-iconbtn" data-act="settings" title="Settings">⚙</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>' +
|
||||
'<span class="pa-title">[AI] Page Agent</span>' +
|
||||
'<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="hide" title="Hide (reopen with ' + HOTKEY.label + ' or a three-finger tap)">x</button>' +
|
||||
'</div>' +
|
||||
'<div class="pa-body">' +
|
||||
'<div class="pa-log"></div>' +
|
||||
'<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>' +
|
||||
'</div>' +
|
||||
'<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;
|
||||
function isHidden() { return panel.classList.contains('pa-hidden'); }
|
||||
@@ -335,7 +335,7 @@
|
||||
sendBtn.dataset.label = sendBtn.textContent;
|
||||
sendBtn.innerHTML = '<span class="pa-spinner"></span>';
|
||||
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.scrollTop = logEl.scrollHeight;
|
||||
} else {
|
||||
@@ -353,13 +353,13 @@
|
||||
|
||||
async function run(request) {
|
||||
if (busy) return;
|
||||
if (!CONFIG.apiKey) { append('⚠ No API key set. Click ⚙ to configure.'); return; }
|
||||
append('› ' + request);
|
||||
setBusy(true, 'thinking…');
|
||||
if (!CONFIG.apiKey) { append('! No API key set. Click [cfg] to configure.'); return; }
|
||||
append('> ' + request);
|
||||
setBusy(true, 'thinking...');
|
||||
try {
|
||||
const { answer, messages } = await agent(request, append, conversation);
|
||||
conversation = messages; // persist for later turns
|
||||
append('🤖 ' + answer);
|
||||
append('[AI] ' + answer);
|
||||
} catch (e) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} 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 =
|
||||
'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, ' +
|
||||
'@description, an appropriate @match, @run-at and any @grant/@connect you need). ' +
|
||||
'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.';
|
||||
|
||||
function extractCode(text) {
|
||||
@@ -436,12 +436,12 @@
|
||||
|
||||
async function convert() {
|
||||
if (busy) return;
|
||||
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
|
||||
setBusy(true, 'converting…');
|
||||
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||
setBusy(true, 'converting...');
|
||||
try {
|
||||
const code = await generateUserscript();
|
||||
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) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
@@ -451,12 +451,12 @@
|
||||
|
||||
async function download() {
|
||||
if (busy) return;
|
||||
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
|
||||
setBusy(true, 'preparing download…');
|
||||
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||
setBusy(true, 'preparing download...');
|
||||
try {
|
||||
const code = lastCode || await generateUserscript();
|
||||
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) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
@@ -474,7 +474,7 @@
|
||||
if (key !== null) { CONFIG.apiKey = key.trim(); await GM.setValue('apiKey', CONFIG.apiKey); }
|
||||
const mdl = prompt('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);
|
||||
collapseBtn.addEventListener('click', () => {
|
||||
const collapsed = panel.classList.toggle('pa-collapsed');
|
||||
collapseBtn.textContent = collapsed ? '▢' : '—';
|
||||
collapseBtn.textContent = collapsed ? '[ ]' : '-';
|
||||
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) => {
|
||||
if (!!e.ctrlKey === HOTKEY.ctrl && !!e.shiftKey === HOTKEY.shift &&
|
||||
!!e.altKey === HOTKEY.alt && e.code === HOTKEY.code) {
|
||||
@@ -503,7 +503,7 @@
|
||||
}
|
||||
}, 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) => {
|
||||
if (e.touches && e.touches.length === 3) {
|
||||
e.preventDefault();
|
||||
@@ -511,6 +511,6 @@
|
||||
}
|
||||
}, { capture: true, passive: false });
|
||||
|
||||
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via ⚙ first. ') + 'Ask me to inspect or act on this page.');
|
||||
showToast('🤖 Page Agent loaded — ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
|
||||
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via [cfg] first. ') + 'Ask me to inspect or act on this page.');
|
||||
showToast('[AI] Page Agent loaded - ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user