mothy-08 commited on
Commit
2cd2e81
·
1 Parent(s): 1316781

Crawler fixed yet again

Browse files
Files changed (2) hide show
  1. api/crawler.py +44 -19
  2. 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
- logger.info(f"Found {len(urls)} URLs, {len(valid_urls)} valid.")
 
 
 
 
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
- # --- ROBUST EXTRACTION STRATEGY ---
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
- // --- Initialization ---
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
- showView("chat");
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
- els.trainBtn.disabled = true;
 
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
- // 2. POLL until ready (The "Wait" Logic)
125
- if (statusText) statusText.textContent = " Building brain...";
126
 
 
127
  const isReady = await pollForIndex(currentUrl);
128
 
129
- if (!isReady) {
130
- throw new Error("Training timed out. Try again.");
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
- } finally {
141
- // Reset UI state
142
- els.trainBtn.disabled = false;
143
  els.trainStatus.classList.add("hidden");
144
- if (statusText) statusText.textContent = " Processing...";
 
145
  }
146
  }
147
 
148
- // Helper: Polls the backend every 2s to see if index is ready
149
  async function pollForIndex(url) {
150
- const maxAttempts = 30; // Wait up to 60 seconds
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, retrying...");
167
  }
168
 
169
- // Wait 2 seconds
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 = ""; // Clear current view
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
- const div = document.createElement("div");
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
- if (name === "chat") {
283
- els.deleteBtn.classList.remove("hidden");
284
- } else {
285
- els.deleteBtn.classList.add("hidden");
286
- }
287
  }
288
 
289
  function addMessage(type, text) {
290
- const div = document.createElement("div");
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));