add URL translation for content extraction, fetch raw over DOM scraping

for known dynamic pages (GitLab CI jobs), fetch the raw/plain text
endpoint instead of scraping a half-loaded DOM. falls back to DOM
extraction with a MutationObserver settle wait when raw isn't accessible.
extractWebpageContent is now async, all callers updated.
This commit is contained in:
russell@unturf.com 2026-02-22 21:42:46 -05:00
parent 1a5b576a7d
commit 4f589f2449
4 changed files with 101 additions and 8 deletions

View file

@ -1,7 +1,73 @@
// Content extraction and processing functionality
// Function to extract text content along with links and metadata from the webpage
export function extractWebpageContent() {
// URL translation rules — map dynamic page patterns to raw/plain text equivalents.
// Each rule: { pattern: RegExp matching the full URL, translate: (match) => rawURL }
const URL_TRANSLATIONS = [
{
// GitLab CI job pages → raw log output
// e.g. https://git.example.com/group/project/-/jobs/12345 → .../jobs/12345/raw
pattern: /^(https?:\/\/[^/]+\/.*\/-\/jobs\/\d+)\/?$/,
translate: (match) => `${match[1]}/raw`,
},
];
// Try to fetch raw content via URL translation (same-origin, cookies included).
// Returns { raw: string } on success, { matched: true } if pattern matched but
// fetch failed (signals a dynamic page worth waiting for), or null if no match.
async function fetchTranslatedContent(url) {
for (const rule of URL_TRANSLATIONS) {
const match = url.match(rule.pattern);
if (match) {
try {
const rawUrl = rule.translate(match);
const response = await fetch(rawUrl, { credentials: "same-origin" });
if (response.ok) {
const text = await response.text();
if (text && text.length > 0) {
return { raw: text };
}
}
} catch (e) {
console.warn("URL translation fetch failed, falling back to DOM:", e);
}
// Pattern matched but fetch failed — caller should wait for DOM to settle
return { matched: true };
}
}
return null;
}
// Wait for DOM mutations to settle (content stops streaming in)
function waitForDOMSettle(timeout = 3000, quiet = 500) {
return new Promise((resolve) => {
let timer;
const observer = new MutationObserver(() => {
clearTimeout(timer);
timer = setTimeout(() => {
observer.disconnect();
resolve();
}, quiet);
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
});
// Start the quiet timer immediately in case DOM is already settled
timer = setTimeout(() => {
observer.disconnect();
resolve();
}, quiet);
// Hard ceiling so we don't wait forever
setTimeout(() => {
observer.disconnect();
resolve();
}, timeout);
});
}
// Extract DOM content — title, metadata, text with links
function extractDOMContent() {
let content = "";
// Extract title
@ -40,3 +106,30 @@ export function extractWebpageContent() {
getTextWithLinks(document.body);
return content.trim();
}
// Extract text content along with links and metadata from the webpage.
// Tries URL translation first (raw/plain text for known dynamic pages),
// falls back to DOM extraction.
export async function extractWebpageContent() {
const result = await fetchTranslatedContent(window.location.href);
// Raw content fetched successfully — use it directly
if (result?.raw) {
let content = "";
const title = document.title;
if (title) {
content += `**Page Title**: ${title}\n\n`;
}
content += `**Source URL**: ${window.location.href}\n\n`;
content += result.raw;
return content.trim();
}
// Pattern matched but raw fetch failed (auth, CORS, etc) —
// this is a known dynamic page, wait for DOM to settle before scraping
if (result?.matched) {
await waitForDOMSettle();
}
return extractDOMContent();
}

View file

@ -5,7 +5,7 @@ import { getUIText } from "./ui-translations.js";
// Function to read the entire page using Hermes
export async function readPageWithHermes(button = null) {
const content = extractWebpageContent();
const content = await extractWebpageContent();
// Update button tooltip instead of creating separate DOM element
if (button) {

View file

@ -295,14 +295,14 @@ export function initializeUncloseaiElements() {
// Widget creation functions imported from widget-library.js
// Initialize the legacy chat interface (for backward compatibility)
export function initializeChatInterface() {
export async function initializeChatInterface() {
// Only initialize if there are legacy elements (chat-container, user-input, etc.)
const legacyElements = document.querySelector(
"#chat-container, #user-input, #chat-box",
);
if (!legacyElements) return;
const pageContent = extractWebpageContent();
const pageContent = await extractWebpageContent();
chatHistory.push({
role: "system",
content: `Here's the content of the webpage: ${pageContent}`,

View file

@ -1177,7 +1177,7 @@ async function openUncloseaiEmbeddedModalNew() {
try {
// Get page content
const { extractWebpageContent } = await import("./content.js");
const pageContent = extractWebpageContent();
const pageContent = await extractWebpageContent();
// Generate TTS
const { speakText } = await import("./tts.js");
@ -1895,7 +1895,7 @@ async function openUncloseaiEmbeddedModalNew() {
console.log("History length:", history ? history.length : 0);
if (history && history.length > 0) {
// Set up page context for existing conversation
const pageContent = extractWebpageContent();
const pageContent = await extractWebpageContent();
const pageTitle = document.title || window.location.hostname;
const conversationContextAppend = `
@ -2324,7 +2324,7 @@ You have complete knowledge of this page content and can reference any details,
try {
// Get page content for context
const pageContent = extractWebpageContent();
const pageContent = await extractWebpageContent();
const pageTitle = document.title || window.location.hostname;
// FIRST: Generate intro message with specialized intro system prompt