mothy-08 commited on
Commit ·
2cd2e81
1
Parent(s): 1316781
Crawler fixed yet again
Browse files- api/crawler.py +44 -19
- chrome-extension/sidepanel.js +46 -120
api/crawler.py
CHANGED
|
@@ -5,16 +5,35 @@ import trafilatura
|
|
| 5 |
from trafilatura.sitemaps import sitemap_search
|
| 6 |
from api.utils import logger, is_valid_url
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def smart_chunk(text: str, chunk_size=1000, overlap=100) -> list[str]:
|
| 10 |
if not text:
|
| 11 |
return []
|
| 12 |
-
|
| 13 |
chunks = []
|
| 14 |
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
|
| 15 |
-
|
| 16 |
current_chunk = ""
|
| 17 |
-
|
| 18 |
for para in paragraphs:
|
| 19 |
if len(current_chunk) + len(para) > chunk_size:
|
| 20 |
if current_chunk:
|
|
@@ -25,13 +44,25 @@ def smart_chunk(text: str, chunk_size=1000, overlap=100) -> list[str]:
|
|
| 25 |
current_chunk = ""
|
| 26 |
else:
|
| 27 |
current_chunk += para + "\n"
|
| 28 |
-
|
| 29 |
if current_chunk:
|
| 30 |
chunks.append(current_chunk.strip())
|
| 31 |
-
|
| 32 |
return chunks
|
| 33 |
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
def crawl_website(base_url: str, limit: int = 25):
|
| 36 |
logger.info(f"Starting crawl for {base_url}")
|
| 37 |
|
|
@@ -47,11 +78,14 @@ def crawl_website(base_url: str, limit: int = 25):
|
|
| 47 |
urls = []
|
| 48 |
|
| 49 |
if not urls:
|
| 50 |
-
logger.warning("No sitemap found (or blocked). Fallback to base URL.")
|
| 51 |
urls = [base_url]
|
| 52 |
|
| 53 |
valid_urls = [u for u in urls if is_valid_url(u, base_url)]
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
count = 0
|
| 57 |
for link in valid_urls:
|
|
@@ -63,14 +97,12 @@ def crawl_website(base_url: str, limit: int = 25):
|
|
| 63 |
response = requests.get(link, headers=headers, timeout=10)
|
| 64 |
|
| 65 |
if response.status_code != 200:
|
| 66 |
-
logger.warning(f"Blocked or missing ({response.status_code}): {link}")
|
| 67 |
continue
|
| 68 |
|
| 69 |
-
#
|
| 70 |
page_title = "Unknown Page"
|
| 71 |
raw_text = ""
|
| 72 |
|
| 73 |
-
# Attempt 1: Bare Extraction (Best Quality)
|
| 74 |
try:
|
| 75 |
result = trafilatura.bare_extraction(
|
| 76 |
response.text, include_comments=False
|
|
@@ -79,17 +111,11 @@ def crawl_website(base_url: str, limit: int = 25):
|
|
| 79 |
page_title = result.get("title", "Unknown Page")
|
| 80 |
raw_text = result["text"]
|
| 81 |
except Exception:
|
| 82 |
-
# If trafilatura crashes (AttributeError, etc.), fail silently and try fallback
|
| 83 |
-
logger.warning(
|
| 84 |
-
f"Metadata extraction failed for {link}, using fallback."
|
| 85 |
-
)
|
| 86 |
pass
|
| 87 |
|
| 88 |
-
# Attempt 2: Fallback Extraction (If Attempt 1 failed)
|
| 89 |
if not raw_text:
|
|
|
|
| 90 |
raw_text = trafilatura.extract(response.text, include_comments=False)
|
| 91 |
-
|
| 92 |
-
# Manual Title Extraction via Regex (since bare_extraction failed)
|
| 93 |
if raw_text:
|
| 94 |
title_match = re.search(
|
| 95 |
r"<title>(.*?)</title>",
|
|
@@ -104,11 +130,10 @@ def crawl_website(base_url: str, limit: int = 25):
|
|
| 104 |
if not raw_text:
|
| 105 |
continue
|
| 106 |
|
| 107 |
-
# Chunking and Context Injection
|
| 108 |
text_chunks = smart_chunk(raw_text)
|
| 109 |
|
| 110 |
contextualized_chunks = [
|
| 111 |
-
f"Source: {page_title}\n\n{chunk}" for chunk in text_chunks
|
| 112 |
]
|
| 113 |
|
| 114 |
yield link, contextualized_chunks
|
|
|
|
| 5 |
from trafilatura.sitemaps import sitemap_search
|
| 6 |
from api.utils import logger, is_valid_url
|
| 7 |
|
| 8 |
+
# 1. PRIORITY KEYWORDS
|
| 9 |
+
# Pages containing these words get crawled FIRST.
|
| 10 |
+
PRIORITY_KEYWORDS = [
|
| 11 |
+
"about",
|
| 12 |
+
"mission",
|
| 13 |
+
"vision",
|
| 14 |
+
"history",
|
| 15 |
+
"values",
|
| 16 |
+
"team",
|
| 17 |
+
"leadership",
|
| 18 |
+
"board",
|
| 19 |
+
"administration",
|
| 20 |
+
"structure",
|
| 21 |
+
"contact",
|
| 22 |
+
"locations",
|
| 23 |
+
"overview",
|
| 24 |
+
"who-we-are",
|
| 25 |
+
"careers",
|
| 26 |
+
"office-of-the-president",
|
| 27 |
+
"executive",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
|
| 31 |
def smart_chunk(text: str, chunk_size=1000, overlap=100) -> list[str]:
|
| 32 |
if not text:
|
| 33 |
return []
|
|
|
|
| 34 |
chunks = []
|
| 35 |
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
|
|
|
|
| 36 |
current_chunk = ""
|
|
|
|
| 37 |
for para in paragraphs:
|
| 38 |
if len(current_chunk) + len(para) > chunk_size:
|
| 39 |
if current_chunk:
|
|
|
|
| 44 |
current_chunk = ""
|
| 45 |
else:
|
| 46 |
current_chunk += para + "\n"
|
|
|
|
| 47 |
if current_chunk:
|
| 48 |
chunks.append(current_chunk.strip())
|
|
|
|
| 49 |
return chunks
|
| 50 |
|
| 51 |
|
| 52 |
+
def get_url_priority(url: str) -> float:
|
| 53 |
+
"""Higher score = Crawled sooner"""
|
| 54 |
+
score = 0
|
| 55 |
+
url_lower = url.lower()
|
| 56 |
+
|
| 57 |
+
for keyword in PRIORITY_KEYWORDS:
|
| 58 |
+
if keyword in url_lower:
|
| 59 |
+
score += 10
|
| 60 |
+
|
| 61 |
+
# Prefer shorter URLs (e.g., /about is better than /news/2023/10/12/title)
|
| 62 |
+
score -= len(url) * 0.05
|
| 63 |
+
return score
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def crawl_website(base_url: str, limit: int = 25):
|
| 67 |
logger.info(f"Starting crawl for {base_url}")
|
| 68 |
|
|
|
|
| 78 |
urls = []
|
| 79 |
|
| 80 |
if not urls:
|
|
|
|
| 81 |
urls = [base_url]
|
| 82 |
|
| 83 |
valid_urls = [u for u in urls if is_valid_url(u, base_url)]
|
| 84 |
+
|
| 85 |
+
# SORT BY IMPORTANCE
|
| 86 |
+
valid_urls.sort(key=get_url_priority, reverse=True)
|
| 87 |
+
|
| 88 |
+
logger.info(f"Found {len(urls)} URLs. Top priority: {valid_urls[:3]}")
|
| 89 |
|
| 90 |
count = 0
|
| 91 |
for link in valid_urls:
|
|
|
|
| 97 |
response = requests.get(link, headers=headers, timeout=10)
|
| 98 |
|
| 99 |
if response.status_code != 200:
|
|
|
|
| 100 |
continue
|
| 101 |
|
| 102 |
+
# Robust Extraction
|
| 103 |
page_title = "Unknown Page"
|
| 104 |
raw_text = ""
|
| 105 |
|
|
|
|
| 106 |
try:
|
| 107 |
result = trafilatura.bare_extraction(
|
| 108 |
response.text, include_comments=False
|
|
|
|
| 111 |
page_title = result.get("title", "Unknown Page")
|
| 112 |
raw_text = result["text"]
|
| 113 |
except Exception:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
pass
|
| 115 |
|
|
|
|
| 116 |
if not raw_text:
|
| 117 |
+
# Fallback manual extraction
|
| 118 |
raw_text = trafilatura.extract(response.text, include_comments=False)
|
|
|
|
|
|
|
| 119 |
if raw_text:
|
| 120 |
title_match = re.search(
|
| 121 |
r"<title>(.*?)</title>",
|
|
|
|
| 130 |
if not raw_text:
|
| 131 |
continue
|
| 132 |
|
|
|
|
| 133 |
text_chunks = smart_chunk(raw_text)
|
| 134 |
|
| 135 |
contextualized_chunks = [
|
| 136 |
+
f"Source: {page_title}\nURL: {link}\n\n{chunk}" for chunk in text_chunks
|
| 137 |
]
|
| 138 |
|
| 139 |
yield link, contextualized_chunks
|
chrome-extension/sidepanel.js
CHANGED
|
@@ -19,29 +19,18 @@ const els = {
|
|
| 19 |
|
| 20 |
// State
|
| 21 |
let currentUrl = "";
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
document.addEventListener("DOMContentLoaded", () => {
|
| 26 |
-
updateContext();
|
| 27 |
-
});
|
| 28 |
-
|
| 29 |
-
chrome.tabs.onActivated.addListener(() => {
|
| 30 |
-
updateContext();
|
| 31 |
-
});
|
| 32 |
-
|
| 33 |
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
| 34 |
-
if (changeInfo.status === "complete" && tab.active)
|
| 35 |
-
updateContext();
|
| 36 |
-
}
|
| 37 |
});
|
| 38 |
|
| 39 |
-
// --- Core Logic ---
|
| 40 |
-
|
| 41 |
async function updateContext() {
|
|
|
|
| 42 |
try {
|
| 43 |
const tab = await getCurrentTab();
|
| 44 |
-
|
| 45 |
if (!tab.url || !tab.url.startsWith("http")) {
|
| 46 |
currentUrl = "";
|
| 47 |
els.domainBadge.textContent = "Restricted";
|
|
@@ -50,17 +39,13 @@ async function updateContext() {
|
|
| 50 |
"Please open a valid website.";
|
| 51 |
return;
|
| 52 |
}
|
| 53 |
-
|
| 54 |
if (tab.url === currentUrl) return;
|
| 55 |
|
| 56 |
currentUrl = tab.url;
|
| 57 |
const urlObj = new URL(currentUrl);
|
| 58 |
els.domainBadge.textContent = urlObj.hostname;
|
| 59 |
|
| 60 |
-
// 1. Load History
|
| 61 |
await loadChatHistory(currentUrl);
|
| 62 |
-
|
| 63 |
-
// 2. Check Status
|
| 64 |
await checkIndexStatus(currentUrl);
|
| 65 |
} catch (err) {
|
| 66 |
console.error(err);
|
|
@@ -69,17 +54,12 @@ async function updateContext() {
|
|
| 69 |
}
|
| 70 |
|
| 71 |
async function checkIndexStatus(url) {
|
| 72 |
-
// CRITICAL FIX:
|
| 73 |
-
// If we already have a conversation history, TRUST IT.
|
| 74 |
-
// Don't show loading screens or re-check the backend.
|
| 75 |
-
// This prevents the "Stuck on Loading" bug when switching tabs.
|
| 76 |
const hasHistory = els.messages.childElementCount > 1;
|
| 77 |
if (hasHistory) {
|
| 78 |
showView("chat");
|
| 79 |
return;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
// Only show "Connecting..." if we are starting fresh
|
| 83 |
showView("loading");
|
| 84 |
document.querySelector("#loading-view p").textContent =
|
| 85 |
"Connecting to Brain...";
|
|
@@ -90,29 +70,24 @@ async function checkIndexStatus(url) {
|
|
| 90 |
headers: { "Content-Type": "application/json" },
|
| 91 |
body: JSON.stringify({ url: url }),
|
| 92 |
});
|
| 93 |
-
|
| 94 |
const data = await res.json();
|
| 95 |
|
| 96 |
-
if (data.exists)
|
| 97 |
-
|
| 98 |
-
} else {
|
| 99 |
-
showView("train");
|
| 100 |
-
}
|
| 101 |
} catch (err) {
|
| 102 |
showError("Backend Offline. Is the Space running?");
|
| 103 |
}
|
| 104 |
}
|
| 105 |
|
| 106 |
async function startTraining() {
|
| 107 |
-
|
|
|
|
| 108 |
els.trainStatus.classList.remove("hidden");
|
| 109 |
|
| 110 |
-
// Update text to show we are working
|
| 111 |
const statusText = els.trainStatus.querySelector("span").nextSibling;
|
| 112 |
if (statusText) statusText.textContent = " Reading website...";
|
| 113 |
|
| 114 |
try {
|
| 115 |
-
// 1. Trigger Ingest
|
| 116 |
const res = await fetch(`${API_BASE}/ingest`, {
|
| 117 |
method: "POST",
|
| 118 |
headers: { "Content-Type": "application/json" },
|
|
@@ -121,33 +96,29 @@ async function startTraining() {
|
|
| 121 |
|
| 122 |
if (!res.ok) throw new Error("Ingest failed");
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
|
|
|
|
| 127 |
const isReady = await pollForIndex(currentUrl);
|
| 128 |
|
| 129 |
-
if (!isReady)
|
| 130 |
-
throw new Error("Training timed out. Try
|
| 131 |
-
}
|
| 132 |
|
| 133 |
-
// 3. Success! Wipe old history and start fresh
|
| 134 |
await clearHistory(false);
|
| 135 |
-
|
| 136 |
showView("chat");
|
| 137 |
addMessage("bot", "I've finished reading! You can ask me questions now.");
|
| 138 |
} catch (err) {
|
| 139 |
alert("Training failed: " + err.message);
|
| 140 |
-
|
| 141 |
-
// Reset UI state
|
| 142 |
-
els.trainBtn.disabled = false;
|
| 143 |
els.trainStatus.classList.add("hidden");
|
| 144 |
-
|
|
|
|
| 145 |
}
|
| 146 |
}
|
| 147 |
|
| 148 |
-
// Helper: Polls the backend every 2s to see if index is ready
|
| 149 |
async function pollForIndex(url) {
|
| 150 |
-
const maxAttempts =
|
| 151 |
let attempts = 0;
|
| 152 |
|
| 153 |
while (attempts < maxAttempts) {
|
|
@@ -158,47 +129,37 @@ async function pollForIndex(url) {
|
|
| 158 |
body: JSON.stringify({ url: url }),
|
| 159 |
});
|
| 160 |
const data = await res.json();
|
| 161 |
-
|
| 162 |
-
if (data.exists && data.vector_count > 0) {
|
| 163 |
-
return true; // Success!
|
| 164 |
-
}
|
| 165 |
} catch (e) {
|
| 166 |
-
console.log("Polling error
|
| 167 |
}
|
| 168 |
|
| 169 |
-
|
| 170 |
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
| 171 |
attempts++;
|
| 172 |
}
|
| 173 |
return false;
|
| 174 |
}
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
async function sendMessage() {
|
| 177 |
const text = els.input.value.trim();
|
| 178 |
if (!text) return;
|
| 179 |
-
|
| 180 |
addMessage("user", text);
|
| 181 |
-
|
| 182 |
els.input.value = "";
|
| 183 |
els.input.disabled = true;
|
| 184 |
els.sendBtn.disabled = true;
|
| 185 |
-
|
| 186 |
const loadingBubble = addLoadingBubble();
|
| 187 |
-
|
| 188 |
try {
|
| 189 |
const res = await fetch(`${API_BASE}/chat`, {
|
| 190 |
method: "POST",
|
| 191 |
headers: { "Content-Type": "application/json" },
|
| 192 |
-
body: JSON.stringify({
|
| 193 |
-
message: text,
|
| 194 |
-
url: currentUrl,
|
| 195 |
-
}),
|
| 196 |
});
|
| 197 |
-
|
| 198 |
if (!res.ok) throw new Error("API Error");
|
| 199 |
-
|
| 200 |
const data = await res.json();
|
| 201 |
-
|
| 202 |
loadingBubble.remove();
|
| 203 |
addMessage("bot", data.answer);
|
| 204 |
} catch (err) {
|
|
@@ -211,61 +172,47 @@ async function sendMessage() {
|
|
| 211 |
}
|
| 212 |
}
|
| 213 |
|
| 214 |
-
// --- Storage & History Logic ---
|
| 215 |
-
|
| 216 |
async function loadChatHistory(url) {
|
| 217 |
-
els.messages.innerHTML = "";
|
| 218 |
-
|
| 219 |
const key = `chat_${url}`;
|
| 220 |
const result = await chrome.storage.local.get(key);
|
| 221 |
const history = result[key] || [];
|
| 222 |
-
|
| 223 |
if (history.length === 0) {
|
| 224 |
const text = "Hello! Ask me anything about this page.";
|
| 225 |
-
|
| 226 |
-
div.className = "msg bot";
|
| 227 |
-
div.innerText = text;
|
| 228 |
-
els.messages.appendChild(div);
|
| 229 |
saveMessageToStorage(url, "bot", text);
|
| 230 |
} else {
|
| 231 |
-
history.forEach((msg) =>
|
| 232 |
-
const div = document.createElement("div");
|
| 233 |
-
div.className = `msg ${msg.type}`;
|
| 234 |
-
div.innerText = msg.text;
|
| 235 |
-
els.messages.appendChild(div);
|
| 236 |
-
});
|
| 237 |
-
scrollToBottom();
|
| 238 |
}
|
| 239 |
}
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
async function saveMessageToStorage(url, type, text) {
|
| 242 |
if (type === "error") return;
|
| 243 |
-
|
| 244 |
const key = `chat_${url}`;
|
| 245 |
const result = await chrome.storage.local.get(key);
|
| 246 |
const history = result[key] || [];
|
| 247 |
-
|
| 248 |
history.push({ type, text, timestamp: Date.now() });
|
| 249 |
-
|
| 250 |
if (history.length > 50) history.shift();
|
| 251 |
-
|
| 252 |
await chrome.storage.local.set({ [key]: history });
|
| 253 |
}
|
| 254 |
|
| 255 |
async function clearHistory(reloadDefault = true) {
|
| 256 |
if (!currentUrl) return;
|
| 257 |
const key = `chat_${currentUrl}`;
|
| 258 |
-
|
| 259 |
await chrome.storage.local.remove(key);
|
| 260 |
els.messages.innerHTML = "";
|
| 261 |
-
|
| 262 |
-
if (reloadDefault) {
|
| 263 |
-
await loadChatHistory(currentUrl);
|
| 264 |
-
}
|
| 265 |
}
|
| 266 |
|
| 267 |
-
// --- Utilities ---
|
| 268 |
-
|
| 269 |
function getCurrentTab() {
|
| 270 |
return new Promise((resolve) => {
|
| 271 |
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
|
|
@@ -278,54 +225,33 @@ function getCurrentTab() {
|
|
| 278 |
function showView(name) {
|
| 279 |
Object.values(views).forEach((el) => el.classList.add("hidden"));
|
| 280 |
views[name].classList.remove("hidden");
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
els.deleteBtn.classList.remove("hidden");
|
| 284 |
-
} else {
|
| 285 |
-
els.deleteBtn.classList.add("hidden");
|
| 286 |
-
}
|
| 287 |
}
|
| 288 |
|
| 289 |
function addMessage(type, text) {
|
| 290 |
-
|
| 291 |
-
div.className = `msg ${type}`;
|
| 292 |
-
div.innerText = text;
|
| 293 |
-
els.messages.appendChild(div);
|
| 294 |
-
scrollToBottom();
|
| 295 |
-
|
| 296 |
saveMessageToStorage(currentUrl, type, text);
|
| 297 |
}
|
| 298 |
|
| 299 |
function addLoadingBubble() {
|
| 300 |
const div = document.createElement("div");
|
| 301 |
div.className = "msg bot loading";
|
| 302 |
-
div.innerHTML = `
|
| 303 |
-
<div class="dot"></div>
|
| 304 |
-
<div class="dot"></div>
|
| 305 |
-
<div class="dot"></div>
|
| 306 |
-
`;
|
| 307 |
els.messages.appendChild(div);
|
| 308 |
scrollToBottom();
|
| 309 |
return div;
|
| 310 |
}
|
| 311 |
|
| 312 |
function scrollToBottom() {
|
| 313 |
-
els.messages.scrollTo({
|
| 314 |
-
top: els.messages.scrollHeight,
|
| 315 |
-
behavior: "smooth",
|
| 316 |
-
});
|
| 317 |
}
|
| 318 |
|
| 319 |
function showError(msg) {
|
| 320 |
-
document.querySelector("main").innerHTML =
|
| 321 |
-
<div class="view">
|
| 322 |
-
<div class="error" style="max-width: 80%">${msg}</div>
|
| 323 |
-
</div>
|
| 324 |
-
`;
|
| 325 |
}
|
| 326 |
|
| 327 |
-
// --- Event Listeners ---
|
| 328 |
-
|
| 329 |
els.trainBtn.addEventListener("click", startTraining);
|
| 330 |
els.sendBtn.addEventListener("click", sendMessage);
|
| 331 |
els.deleteBtn.addEventListener("click", () => clearHistory(true));
|
|
|
|
| 19 |
|
| 20 |
// State
|
| 21 |
let currentUrl = "";
|
| 22 |
+
let isTraining = false;
|
| 23 |
|
| 24 |
+
document.addEventListener("DOMContentLoaded", () => updateContext());
|
| 25 |
+
chrome.tabs.onActivated.addListener(() => updateContext());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
| 27 |
+
if (changeInfo.status === "complete" && tab.active) updateContext();
|
|
|
|
|
|
|
| 28 |
});
|
| 29 |
|
|
|
|
|
|
|
| 30 |
async function updateContext() {
|
| 31 |
+
if (isTraining) return;
|
| 32 |
try {
|
| 33 |
const tab = await getCurrentTab();
|
|
|
|
| 34 |
if (!tab.url || !tab.url.startsWith("http")) {
|
| 35 |
currentUrl = "";
|
| 36 |
els.domainBadge.textContent = "Restricted";
|
|
|
|
| 39 |
"Please open a valid website.";
|
| 40 |
return;
|
| 41 |
}
|
|
|
|
| 42 |
if (tab.url === currentUrl) return;
|
| 43 |
|
| 44 |
currentUrl = tab.url;
|
| 45 |
const urlObj = new URL(currentUrl);
|
| 46 |
els.domainBadge.textContent = urlObj.hostname;
|
| 47 |
|
|
|
|
| 48 |
await loadChatHistory(currentUrl);
|
|
|
|
|
|
|
| 49 |
await checkIndexStatus(currentUrl);
|
| 50 |
} catch (err) {
|
| 51 |
console.error(err);
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
async function checkIndexStatus(url) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
const hasHistory = els.messages.childElementCount > 1;
|
| 58 |
if (hasHistory) {
|
| 59 |
showView("chat");
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
|
|
|
|
| 63 |
showView("loading");
|
| 64 |
document.querySelector("#loading-view p").textContent =
|
| 65 |
"Connecting to Brain...";
|
|
|
|
| 70 |
headers: { "Content-Type": "application/json" },
|
| 71 |
body: JSON.stringify({ url: url }),
|
| 72 |
});
|
|
|
|
| 73 |
const data = await res.json();
|
| 74 |
|
| 75 |
+
if (data.exists) showView("chat");
|
| 76 |
+
else showView("train");
|
|
|
|
|
|
|
|
|
|
| 77 |
} catch (err) {
|
| 78 |
showError("Backend Offline. Is the Space running?");
|
| 79 |
}
|
| 80 |
}
|
| 81 |
|
| 82 |
async function startTraining() {
|
| 83 |
+
isTraining = true;
|
| 84 |
+
els.trainBtn.classList.add("hidden");
|
| 85 |
els.trainStatus.classList.remove("hidden");
|
| 86 |
|
|
|
|
| 87 |
const statusText = els.trainStatus.querySelector("span").nextSibling;
|
| 88 |
if (statusText) statusText.textContent = " Reading website...";
|
| 89 |
|
| 90 |
try {
|
|
|
|
| 91 |
const res = await fetch(`${API_BASE}/ingest`, {
|
| 92 |
method: "POST",
|
| 93 |
headers: { "Content-Type": "application/json" },
|
|
|
|
| 96 |
|
| 97 |
if (!res.ok) throw new Error("Ingest failed");
|
| 98 |
|
| 99 |
+
if (statusText)
|
| 100 |
+
statusText.textContent = " Building brain (this may take a few mins)...";
|
| 101 |
|
| 102 |
+
// INCREASED TIMEOUT: 5 Minutes
|
| 103 |
const isReady = await pollForIndex(currentUrl);
|
| 104 |
|
| 105 |
+
if (!isReady)
|
| 106 |
+
throw new Error("Training timed out. Try refreshing the page.");
|
|
|
|
| 107 |
|
|
|
|
| 108 |
await clearHistory(false);
|
|
|
|
| 109 |
showView("chat");
|
| 110 |
addMessage("bot", "I've finished reading! You can ask me questions now.");
|
| 111 |
} catch (err) {
|
| 112 |
alert("Training failed: " + err.message);
|
| 113 |
+
els.trainBtn.classList.remove("hidden");
|
|
|
|
|
|
|
| 114 |
els.trainStatus.classList.add("hidden");
|
| 115 |
+
} finally {
|
| 116 |
+
isTraining = false;
|
| 117 |
}
|
| 118 |
}
|
| 119 |
|
|
|
|
| 120 |
async function pollForIndex(url) {
|
| 121 |
+
const maxAttempts = 100; // 100 * 3s = 300s (5 mins)
|
| 122 |
let attempts = 0;
|
| 123 |
|
| 124 |
while (attempts < maxAttempts) {
|
|
|
|
| 129 |
body: JSON.stringify({ url: url }),
|
| 130 |
});
|
| 131 |
const data = await res.json();
|
| 132 |
+
if (data.exists && data.vector_count > 0) return true;
|
|
|
|
|
|
|
|
|
|
| 133 |
} catch (e) {
|
| 134 |
+
console.log("Polling error...");
|
| 135 |
}
|
| 136 |
|
| 137 |
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
|
|
| 138 |
attempts++;
|
| 139 |
}
|
| 140 |
return false;
|
| 141 |
}
|
| 142 |
|
| 143 |
+
// ... (Keep sendMessage, loadChatHistory, saveMessageToStorage, clearHistory, getCurrentTab, showView, addMessage, addLoadingBubble, scrollToBottom, showError, Event Listeners exactly as they were) ...
|
| 144 |
+
// For brevity, I am not repeating the helper functions here, but make sure you keep them!
|
| 145 |
+
|
| 146 |
+
// --- Utilities (Shortened for copy-paste context) ---
|
| 147 |
async function sendMessage() {
|
| 148 |
const text = els.input.value.trim();
|
| 149 |
if (!text) return;
|
|
|
|
| 150 |
addMessage("user", text);
|
|
|
|
| 151 |
els.input.value = "";
|
| 152 |
els.input.disabled = true;
|
| 153 |
els.sendBtn.disabled = true;
|
|
|
|
| 154 |
const loadingBubble = addLoadingBubble();
|
|
|
|
| 155 |
try {
|
| 156 |
const res = await fetch(`${API_BASE}/chat`, {
|
| 157 |
method: "POST",
|
| 158 |
headers: { "Content-Type": "application/json" },
|
| 159 |
+
body: JSON.stringify({ message: text, url: currentUrl }),
|
|
|
|
|
|
|
|
|
|
| 160 |
});
|
|
|
|
| 161 |
if (!res.ok) throw new Error("API Error");
|
|
|
|
| 162 |
const data = await res.json();
|
|
|
|
| 163 |
loadingBubble.remove();
|
| 164 |
addMessage("bot", data.answer);
|
| 165 |
} catch (err) {
|
|
|
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
| 175 |
async function loadChatHistory(url) {
|
| 176 |
+
els.messages.innerHTML = "";
|
|
|
|
| 177 |
const key = `chat_${url}`;
|
| 178 |
const result = await chrome.storage.local.get(key);
|
| 179 |
const history = result[key] || [];
|
|
|
|
| 180 |
if (history.length === 0) {
|
| 181 |
const text = "Hello! Ask me anything about this page.";
|
| 182 |
+
addMessageUI("bot", text);
|
|
|
|
|
|
|
|
|
|
| 183 |
saveMessageToStorage(url, "bot", text);
|
| 184 |
} else {
|
| 185 |
+
history.forEach((msg) => addMessageUI(msg.type, msg.text));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
}
|
| 188 |
|
| 189 |
+
function addMessageUI(type, text) {
|
| 190 |
+
// Logic split for clarity
|
| 191 |
+
const div = document.createElement("div");
|
| 192 |
+
div.className = `msg ${type}`;
|
| 193 |
+
div.innerText = text;
|
| 194 |
+
els.messages.appendChild(div);
|
| 195 |
+
scrollToBottom();
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
async function saveMessageToStorage(url, type, text) {
|
| 199 |
if (type === "error") return;
|
|
|
|
| 200 |
const key = `chat_${url}`;
|
| 201 |
const result = await chrome.storage.local.get(key);
|
| 202 |
const history = result[key] || [];
|
|
|
|
| 203 |
history.push({ type, text, timestamp: Date.now() });
|
|
|
|
| 204 |
if (history.length > 50) history.shift();
|
|
|
|
| 205 |
await chrome.storage.local.set({ [key]: history });
|
| 206 |
}
|
| 207 |
|
| 208 |
async function clearHistory(reloadDefault = true) {
|
| 209 |
if (!currentUrl) return;
|
| 210 |
const key = `chat_${currentUrl}`;
|
|
|
|
| 211 |
await chrome.storage.local.remove(key);
|
| 212 |
els.messages.innerHTML = "";
|
| 213 |
+
if (reloadDefault) await loadChatHistory(currentUrl);
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
|
|
|
|
|
|
|
| 216 |
function getCurrentTab() {
|
| 217 |
return new Promise((resolve) => {
|
| 218 |
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
|
|
|
|
| 225 |
function showView(name) {
|
| 226 |
Object.values(views).forEach((el) => el.classList.add("hidden"));
|
| 227 |
views[name].classList.remove("hidden");
|
| 228 |
+
if (name === "chat") els.deleteBtn.classList.remove("hidden");
|
| 229 |
+
else els.deleteBtn.classList.add("hidden");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
function addMessage(type, text) {
|
| 233 |
+
addMessageUI(type, text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
saveMessageToStorage(currentUrl, type, text);
|
| 235 |
}
|
| 236 |
|
| 237 |
function addLoadingBubble() {
|
| 238 |
const div = document.createElement("div");
|
| 239 |
div.className = "msg bot loading";
|
| 240 |
+
div.innerHTML = `<div class="dot"></div><div class="dot"></div><div class="dot"></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
els.messages.appendChild(div);
|
| 242 |
scrollToBottom();
|
| 243 |
return div;
|
| 244 |
}
|
| 245 |
|
| 246 |
function scrollToBottom() {
|
| 247 |
+
els.messages.scrollTo({ top: els.messages.scrollHeight, behavior: "smooth" });
|
|
|
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
function showError(msg) {
|
| 251 |
+
document.querySelector("main").innerHTML =
|
| 252 |
+
`<div class="view"><div class="error" style="max-width: 80%">${msg}</div></div>`;
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
|
|
|
|
|
|
| 255 |
els.trainBtn.addEventListener("click", startTraining);
|
| 256 |
els.sendBtn.addEventListener("click", sendMessage);
|
| 257 |
els.deleteBtn.addEventListener("click", () => clearHistory(true));
|