Summarize the current page’s URL using a configurable Ollama API endpoint.
// ==UserScript==
// @name Ollama URL Summarizer
// @namespace https://greasyfork.org/en/users/807108-jeremy-r
// @version 1.4.0
// @description Summarize the current page’s URL using a configurable Ollama API endpoint.
// @author JRem
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect *
// @run-at document-idle
// @license MIT
// ==/UserScript==
/* --------------------------------------------------------------
CONFIGURATION STORAGE & DEFAULTS
-------------------------------------------------------------- */
const DEFAULTS = {
// --------------------------------------------------------------
// 1️⃣ Ollama endpoint – where the POST request will be sent.
// Example (local): http://localhost:11434/api/chat
// Example (remote): https://my-ollama-proxy.com/v1/chat/completions
// --------------------------------------------------------------
endpoint: "http://localhost:11434/api/chat",
// --------------------------------------------------------------
// 2️⃣ API key (if the endpoint needs authentication)
// Using Ollama-Gateway to implement an API token
// --------------------------------------------------------------
apiKey: "",
// --------------------------------------------------------------
// 3️⃣ Model name
// --------------------------------------------------------------
model: "granite3.3:2b",
// --------------------------------------------------------------
// 4️⃣ Prompt – now expects a {{url}} placeholder
// --------------------------------------------------------------
systemPrompt:
"You are a concise assistant. Summarize the following webpage content in no more than 3 short paragraphs, preserving the main ideas and any important facts. {{content}",
// --------------------------------------------------------------
// 5️⃣ Generation parameters
// --------------------------------------------------------------
temperature: 0.6,
maxTokens: 2046,
// ------------------------------------------------------------------
// 6️⃣ How much of the page text to send (characters). Ollama’s default
// context window is 4 KB–128 KB depending on model, so we cap it.
// ------------------------------------------------------------------
maxPageChars: 15000
};
/* ----------------------------------------------------------------------
Utility helpers
---------------------------------------------------------------------- */
function getSetting(key) {
return GM_getValue(key, DEFAULTS[key]);
}
function setSetting(key, value) {
GM_setValue(key, value);
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
/* ----------------------------------------------------------------------
UI: modal for displaying the summary
---------------------------------------------------------------------- */
GM_addStyle(`
.ollama-sum-modal {
position: fixed; inset: 0; background: rgba(0,0,0,.6);
display: flex; align-items: center; justify-content: center;
z-index: 999999;
}
.ollama-sum-box {
background:#fff; color:#111; max-width:800px; max-height:80vh;
overflow:auto; padding:1.5rem; border-radius:8px; box-shadow:0 4px 15px rgba(0,0,0,.3);
font-family:system-ui,Arial,sans-serif; line-height:1.5;
}
.ollama-sum-close {
position:absolute; top:.5rem; right:.7rem; cursor:pointer;
font-size:1.4rem; background:none; border:none; color:#555;
}
.ollama-sum-close:hover {color:#000;}
`);
function showModal(htmlContent) {
const overlay = document.createElement("div");
overlay.className = "ollama-sum-modal";
const box = document.createElement("div");
box.className = "ollama-sum-box";
box.innerHTML = `
<button class="ollama-sum-close" title="Close">×</button>
${htmlContent}
`;
overlay.appendChild(box);
document.body.appendChild(overlay);
overlay.querySelector(".ollama-sum-close").onclick = () => overlay.remove();
}
/* ----------------------------------------------------------------------
UI: Settings dialog
---------------------------------------------------------------------- */
function openSettingsDialog() {
const form = document.createElement("form");
form.innerHTML = `
<style>
.ollama-set-table {width:100%; border-collapse:collapse;}
.ollama-set-table td {padding:.4rem;}
.ollama-set-table input, .ollama-set-table textarea {width:100%; box-sizing:border-box;}
.ollama-set-table textarea {height:80px; resize:vertical;}
.ollama-set-submit {margin-top:.8rem; padding:.5rem 1rem;}
</style>
<h2>Ollama Summarizer – Settings</h2>
<table class="ollama-set-table">
<tr><td>Endpoint URL</td>
<td><input type="url" name="endpoint" required></td></tr>
<tr><td>API Key (optional)</td>
<td><input type="text" name="apiKey"></td></tr>
<tr><td>Model</td>
<td><input type="text" name="model" required></td></tr>
<tr><td>System Prompt<br><small>Use <code>{{content}}</code> as placeholder.</small></td>
<td><textarea name="systemPrompt" required></textarea></td></tr>
<tr><td>Temperature (0‑1)</td>
<td><input type="number" name="temperature" step="0.01" min="0" max="1"></td></tr>
<tr><td>Max tokens</td>
<td><input type="number" name="maxTokens" min="1"></td></tr>
<tr><td>Max page characters sent</td>
<td><input type="number" name="maxPageChars" min="1000"></td></tr>
</table>
<button type="submit" class="ollama-set-submit">Save</button>
`;
// Populate current values
for (const key of Object.keys(DEFAULTS)) {
const el = form.querySelector(`[name="${key}"]`);
if (el) el.value = getSetting(key);
}
form.onsubmit = e => {
e.preventDefault();
const data = new FormData(form);
for (const [k, v] of data.entries()) {
// Numbers need conversion
if (["temperature", "maxTokens", "maxPageChars"].includes(k))
setSetting(k, Number(v));
else
setSetting(k, v.trim());
}
alert("Settings saved! The next summarise will use the new values.");
overlay.remove();
};
const overlay = document.createElement("div");
overlay.className = "ollama-sum-modal";
overlay.innerHTML = `<div class="ollama-sum-box"></div>`;
overlay.querySelector(".ollama-sum-box").appendChild(form);
document.body.appendChild(overlay);
}
/* ----------------------------------------------------------------------
Core: extract visible page text (with size limit)
---------------------------------------------------------------------- */
function getPageText() {
// Clone the document body, strip scripts/styles, then get innerText.
const clone = document.body.cloneNode(true);
// Remove script/style/meta/iframe elements that add noise
//clone.querySelectorAll("script,style,meta,iframe,noscript").forEach(el => el.remove());
clone.querySelectorAll("script,style,noscript").forEach(el => el.remove());
const text = clone.innerText.trim().replace(/\s+/g, " ");
const limit = getSetting("maxPageChars");
if (text.length > limit) {
return text.slice(0, limit) + "…";
}
return text;
}
/* ----------------------------------------------------------------------
Core: call Ollama API
---------------------------------------------------------------------- */
async function callOllama(pageContent) {
const endpoint = getSetting("endpoint");
const apiKey = getSetting("apiKey");
const model = getSetting("model");
const temperature = getSetting("temperature");
const maxTokens = getSetting("maxTokens");
const systemPromptTemplate = getSetting("systemPrompt");
// Insert the page content into the system prompt (or fallback to user message)
const systemPrompt = systemPromptTemplate.replace("{{content}}", pageContent);
// Ollama supports two flavors:
// 1) /api/chat (legacy) – expects {model, messages, stream:false, options}
// 2) OpenAI‑compatible /v1/chat/completions – expects messages array.
// We'll try the /api/chat format first; if the endpoint ends with /v1/... we adapt.
const isOpenAI = /\/v1\//i.test(endpoint);
const body = isOpenAI
? {
model,
messages: [
{ role: "system", content: systemPrompt }
// No user message – the whole prompt is already in system.
],
temperature,
max_tokens: maxTokens,
stream: false
}
: {
model,
messages: [
{ role: "system", content: systemPrompt }
],
stream: false,
options: {
temperature,
num_ctx: maxTokens // Ollama treats this as max context tokens (optional)
}
};
// GM_xmlhttpRequest is used because some sites have CSP that blocks fetch.
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: endpoint,
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
},
data: JSON.stringify(body),
timeout: 60_000,
onload: resp => {
if (resp.status >= 200 && resp.status < 300) {
try {
const json = JSON.parse(resp.responseText);
// Different formats: Ollama returns {message:{content:...}}; OpenAI returns {choices:[{message:{content:...}}]}
let content;
if (json.message && json.message.content) {
content = json.message.content;
} else if (json.choices && json.choices[0]?.message?.content) {
content = json.choices[0].message.content;
} else if (json.response) { // legacy Ollama non‑stream response
content = json.response;
} else {
throw new Error("Unexpected response format");
}
resolve(content.trim());
} catch (e) {
reject(`Failed to parse response: ${e.message}`);
}
} else {
reject(`HTTP ${resp.status}: ${resp.statusText}\n${resp.responseText}`);
}
},
onerror: err => reject(`Network error: ${err}`),
ontimeout: () => reject("Request timed out")
});
});
}
/* ----------------------------------------------------------------------
UI: the floating “Summarize” button
---------------------------------------------------------------------- */
function createSummarizeButton() {
const btn = document.createElement("button");
btn.textContent = "📰 Summarize page";
btn.title = "Generate a short summary using Ollama";
btn.style.position = "fixed";
btn.style.bottom = "20px";
btn.style.right = "20px";
btn.style.zIndex = "999999";
btn.style.padding = "0.6rem 1rem";
btn.style.background = "#0066ff";
btn.style.color = "#fff";
btn.style.border = "none";
btn.style.borderRadius = "4px";
btn.style.boxShadow = "0 2px 6px rgba(0,0,0,.2)";
btn.style.cursor = "pointer";
btn.style.fontSize = "0.9rem";
btn.onclick = async () => {
btn.disabled = true;
btn.textContent = "⏳ Summarising…";
try {
const pageText = getPageText();
const summary = await callOllama(pageText);
showModal(`
<h2>Page summary</h2>
<p>${escapeHtml(summary).replace(/\n/g, "<br>")}</p>
`);
} catch (e) {
showModal(`
<h2 style="color:#c00;">Error</h2>
<pre>${escapeHtml(String(e))}</pre>
`);
} finally {
btn.disabled = false;
btn.textContent = "📰 Summarize page";
}
};
document.body.appendChild(btn);
}
/* ----------------------------------------------------------------------
Register menu commands (Tampermonkey/Greasemonkey)
---------------------------------------------------------------------- */
GM_registerMenuCommand("⚙️ Ollama Summarizer Settings", openSettingsDialog);
GM_registerMenuCommand("🧹 Reset to defaults", () => {
if (confirm("Reset all Ollama Summarizer settings to default values?")) {
for (const k of Object.keys(DEFAULTS)) GM_setValue(k, DEFAULTS[k]);
alert("Defaults restored – reload the page for the changes to take effect.");
}
});
/* ----------------------------------------------------------------------
Initialise
---------------------------------------------------------------------- */
(function () {
// Delay a little to avoid fighting with heavy‑weight SPAs that rewrite the DOM
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(createSummarizeButton, 1000);
} else {
window.addEventListener("load", () => setTimeout(createSummarizeButton, 1000));
}
})();