const API_URL = "http://127.0.0.1:19827";
const statusBar = document.getElementById("statusBar");
const titleInput = document.getElementById("titleInput");
const urlPreview = document.getElementById("urlPreview");
const contentPreview = document.getElementById("contentPreview");
const clipBtn = document.getElementById("clipBtn");
const projectSelect = document.getElementById("projectSelect");
let extractedContent = "";
let pageUrl = "";
async function checkConnection() {
try {
const res = await fetch(`${API_URL}/status`, { method: "GET" });
const data = await res.json();
if (data.ok) {
statusBar.className = "status connected";
statusBar.textContent = "✓ Connected to LLM Wiki";
await loadProjects();
return true;
}
} catch {}
statusBar.className = "status disconnected";
statusBar.textContent = "✗ LLM Wiki app is not running";
clipBtn.disabled = true;
projectSelect.innerHTML = '';
return false;
}
async function loadProjects() {
try {
const res = await fetch(`${API_URL}/projects`, { method: "GET" });
const data = await res.json();
if (data.ok && data.projects?.length > 0) {
projectSelect.innerHTML = "";
for (const proj of data.projects) {
const opt = document.createElement("option");
opt.value = proj.path;
opt.textContent = proj.name + (proj.current ? " (current)" : "");
if (proj.current) opt.selected = true;
projectSelect.appendChild(opt);
}
return;
}
} catch {}
// Fallback to current project
try {
const res = await fetch(`${API_URL}/project`, { method: "GET" });
const data = await res.json();
if (data.ok && data.path) {
const name = data.path.replace(/\\/g, "/").split("/").pop() || data.path;
projectSelect.innerHTML = ``;
}
} catch {
projectSelect.innerHTML = '';
}
}
async function extractContent() {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) return;
pageUrl = tab.url || "";
titleInput.value = tab.title || "Untitled";
urlPreview.textContent = pageUrl;
// First inject Readability.js and Turndown.js into the page
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["Readability.js", "Turndown.js"],
});
// Then extract content using them
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
try {
// Use Readability to extract article content
const documentClone = document.cloneNode(true);
const reader = new window.Readability(documentClone);
const article = reader.parse();
if (!article || !article.content) {
return { error: "Readability could not extract content" };
}
// Use Turndown to convert HTML to Markdown
const turndown = new window.TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
bulletListMarker: "-",
});
// Add table support
turndown.addRule("tableCell", {
filter: ["th", "td"],
replacement: (content) => ` ${content.trim()} |`,
});
turndown.addRule("tableRow", {
filter: "tr",
replacement: (content) => `|${content}\n`,
});
turndown.addRule("table", {
filter: "table",
replacement: (content) => {
// Add header separator after first row
const lines = content.trim().split("\n");
if (lines.length > 0) {
const cols = (lines[0].match(/\|/g) || []).length - 1;
const separator = "|" + " --- |".repeat(cols);
lines.splice(1, 0, separator);
}
return "\n\n" + lines.join("\n") + "\n\n";
},
});
// Remove images that are tracking pixels or tiny
turndown.addRule("removeSmallImages", {
filter: (node) => {
if (node.nodeName !== "IMG") return false;
const w = parseInt(node.getAttribute("width") || "999");
const h = parseInt(node.getAttribute("height") || "999");
return w < 10 || h < 10;
},
replacement: () => "",
});
const markdown = turndown.turndown(article.content);
return {
title: article.title,
content: markdown,
excerpt: article.excerpt || "",
siteName: article.siteName || "",
length: article.length || 0,
};
} catch (err) {
return { error: err.message };
}
},
});
if (results?.[0]?.result) {
const result = results[0].result;
if (result.error) {
contentPreview.textContent = `Extraction failed: ${result.error}. Falling back...`;
await fallbackExtract(tab.id);
return;
}
// Use Readability's title if better
if (result.title && result.title.length > 5) {
titleInput.value = result.title;
}
extractedContent = result.content;
contentPreview.textContent = extractedContent;
if (result.excerpt) {
contentPreview.textContent = "📝 " + result.excerpt + "\n\n---\n\n" + extractedContent;
}
clipBtn.disabled = false;
} else {
await fallbackExtract(tab.id);
}
} catch (err) {
contentPreview.textContent = `Error: ${err.message}`;
}
}
// Fallback: simple DOM extraction if Readability fails
async function fallbackExtract(tabId) {
const results = await chrome.scripting.executeScript({
target: { tabId },
func: () => {
const clone = document.body.cloneNode(true);
["script", "style", "nav", "header", "footer", ".sidebar", ".ad", ".comments"]
.forEach((sel) => clone.querySelectorAll(sel).forEach((el) => el.remove()));
return clone.innerText
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0)
.join("\n\n")
.slice(0, 50000);
},
});
if (results?.[0]?.result) {
extractedContent = results[0].result;
contentPreview.textContent = extractedContent;
clipBtn.disabled = false;
} else {
contentPreview.textContent = "Failed to extract content";
}
}
async function sendClip() {
const selectedProject = projectSelect.value;
if (!selectedProject) {
statusBar.className = "status error";
statusBar.textContent = "✗ Please select a project";
return;
}
clipBtn.disabled = true;
statusBar.className = "status sending";
statusBar.textContent = "⏳ Sending to LLM Wiki...";
try {
const res = await fetch(`${API_URL}/clip`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: titleInput.value,
url: pageUrl,
content: extractedContent,
projectPath: selectedProject,
}),
});
const data = await res.json();
if (data.ok) {
const projectName = projectSelect.options[projectSelect.selectedIndex]?.textContent || "project";
statusBar.className = "status success";
statusBar.textContent = `✓ Saved to ${projectName}`;
clipBtn.textContent = "✓ Clipped!";
} else {
statusBar.className = "status error";
statusBar.textContent = `✗ Error: ${data.error}`;
clipBtn.disabled = false;
}
} catch (err) {
statusBar.className = "status error";
statusBar.textContent = `✗ Connection failed: ${err.message}`;
clipBtn.disabled = false;
}
}
clipBtn.addEventListener("click", sendClip);
// Resize content preview to fill available space without causing popup scroll
function resizePreview() {
const totalHeight = 500; // matches html/body height
const preview = document.getElementById("contentPreview");
if (!preview) return;
// Calculate space used by everything except the preview
const previewRect = preview.getBoundingClientRect();
const bottomSpace = totalHeight - previewRect.top - 60; // 60px for button + footer
const maxH = Math.max(100, Math.min(300, bottomSpace));
preview.style.maxHeight = maxH + "px";
}
(async () => {
const connected = await checkConnection();
// Always extract content so user can preview, even if app not running
await extractContent();
if (!connected) {
clipBtn.disabled = true;
clipBtn.textContent = "📎 App not running — cannot save";
}
setTimeout(resizePreview, 100);
})();