jobbler commited on
Commit
be6166b
·
1 Parent(s): 5929fc6

feat: Add download status polling to UI and Firefox extension

Browse files
firefox-extension/content.js CHANGED
@@ -48,6 +48,26 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
48
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
49
 
50
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  // Connect to the configured API Space
52
  const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
53
  method: 'POST',
@@ -59,6 +79,7 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
59
  layers: checkedLayers
60
  })
61
  });
 
62
 
63
  if (!response.ok) throw new Error('API error');
64
 
@@ -159,9 +180,32 @@ async function processEntirePage() {
159
  const batchSize = 3;
160
  let processedCount = 0;
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  for (let i = 0; i < nodesToProcess.length; i += batchSize) {
163
  const batch = nodesToProcess.slice(i, i + batchSize);
164
- showToast(`FlowRead analyzing page (${processedCount}/${nodesToProcess.length} blocks)...`, 0);
 
 
 
 
 
165
 
166
  await Promise.all(batch.map(async (node) => {
167
  const text = node.nodeValue;
@@ -206,6 +250,7 @@ async function processEntirePage() {
206
  processedCount += batch.length;
207
  }
208
 
 
209
  showToast(`Done! Analyzed ${processedCount} blocks.`, 2000);
210
  }
211
 
@@ -223,6 +268,7 @@ async function updateExisting(newSettings) {
223
 
224
  let reFetchCount = 0;
225
  let rerenderCount = 0;
 
226
 
227
  for (const container of containers) {
228
  const oldPreprompt = container.dataset.preprompt || "";
@@ -234,6 +280,21 @@ async function updateExisting(newSettings) {
234
  if (oldPreprompt !== preprompt || oldMode !== saliencyMode || oldModelVersion !== modelVersion) {
235
  if (reFetchCount === 0) {
236
  showToast("Updating FlowRead elements with new settings...", 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
238
  try {
239
  const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
@@ -274,6 +335,8 @@ async function updateExisting(newSettings) {
274
  }
275
  }
276
 
 
 
277
  if (reFetchCount > 0) {
278
  showToast(`Updated ${reFetchCount} blocks with new AI intent!`, 2000);
279
  } else if (rerenderCount > 0) {
 
48
  const checkedLayers = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
49
 
50
  try {
51
+ // Start polling status
52
+ let isFetching = true;
53
+ const pollStatus = async () => {
54
+ while (isFetching) {
55
+ try {
56
+ const statusRes = await fetch(`${apiUrl}/status`);
57
+ if (statusRes.ok) {
58
+ const statusData = await statusRes.json();
59
+ if (statusData[modelVersion] === "downloading") {
60
+ showToast(`Downloading Gemma 4 (${modelVersion})... this may take a few minutes.`, 0);
61
+ }
62
+ }
63
+ } catch (e) {
64
+ // ignore network errors for status polling
65
+ }
66
+ await new Promise(r => setTimeout(r, 2000));
67
+ }
68
+ };
69
+ pollStatus();
70
+
71
  // Connect to the configured API Space
72
  const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
73
  method: 'POST',
 
79
  layers: checkedLayers
80
  })
81
  });
82
+ isFetching = false;
83
 
84
  if (!response.ok) throw new Error('API error');
85
 
 
180
  const batchSize = 3;
181
  let processedCount = 0;
182
 
183
+ // Polling logic for first request
184
+ let isFetchingStatus = true;
185
+ const pollStatus = async () => {
186
+ while (isFetchingStatus) {
187
+ try {
188
+ const statusRes = await fetch(`${apiUrl}/status`);
189
+ if (statusRes.ok) {
190
+ const statusData = await statusRes.json();
191
+ if (statusData[modelVersion] === "downloading") {
192
+ showToast(`Downloading Gemma 4 (${modelVersion})... this may take a few minutes.`, 0);
193
+ }
194
+ }
195
+ } catch (e) {}
196
+ await new Promise(r => setTimeout(r, 2000));
197
+ }
198
+ };
199
+ pollStatus();
200
+
201
  for (let i = 0; i < nodesToProcess.length; i += batchSize) {
202
  const batch = nodesToProcess.slice(i, i + batchSize);
203
+
204
+ // Only show analyzing text if not downloading
205
+ const statusResTemp = await fetch(`${apiUrl}/status`).catch(() => null);
206
+ if (!statusResTemp || !statusResTemp.ok || (await statusResTemp.json())[modelVersion] !== "downloading") {
207
+ showToast(`FlowRead analyzing page (${processedCount}/${nodesToProcess.length} blocks)...`, 0);
208
+ }
209
 
210
  await Promise.all(batch.map(async (node) => {
211
  const text = node.nodeValue;
 
250
  processedCount += batch.length;
251
  }
252
 
253
+ isFetchingStatus = false;
254
  showToast(`Done! Analyzed ${processedCount} blocks.`, 2000);
255
  }
256
 
 
268
 
269
  let reFetchCount = 0;
270
  let rerenderCount = 0;
271
+ let isFetchingStatus = false;
272
 
273
  for (const container of containers) {
274
  const oldPreprompt = container.dataset.preprompt || "";
 
280
  if (oldPreprompt !== preprompt || oldMode !== saliencyMode || oldModelVersion !== modelVersion) {
281
  if (reFetchCount === 0) {
282
  showToast("Updating FlowRead elements with new settings...", 0);
283
+ isFetchingStatus = true;
284
+ (async () => {
285
+ while (isFetchingStatus) {
286
+ try {
287
+ const statusRes = await fetch(`${apiUrl}/status`);
288
+ if (statusRes.ok) {
289
+ const statusData = await statusRes.json();
290
+ if (statusData[modelVersion] === "downloading") {
291
+ showToast(`Downloading Gemma 4 (${modelVersion})... this may take a few minutes.`, 0);
292
+ }
293
+ }
294
+ } catch (e) {}
295
+ await new Promise(r => setTimeout(r, 2000));
296
+ }
297
+ })();
298
  }
299
  try {
300
  const response = await fetch(`${apiUrl}/analyze/${modelVersion}`, {
 
335
  }
336
  }
337
 
338
+ isFetchingStatus = false;
339
+
340
  if (reFetchCount > 0) {
341
  showToast(`Updated ${reFetchCount} blocks with new AI intent!`, 2000);
342
  } else if (rerenderCount > 0) {
flowread-extension.zip CHANGED
Binary files a/flowread-extension.zip and b/flowread-extension.zip differ
 
main.py CHANGED
@@ -236,6 +236,10 @@ def get_study_stats():
236
  # --- Saliency API (Existing) ---
237
  models = {}
238
  tokenizers = {}
 
 
 
 
239
 
240
  device = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
241
  hf_token = os.environ.get("HF_TOKEN")
@@ -245,6 +249,7 @@ def load_model(model_name: str):
245
  return models[model_name], tokenizers[model_name]
246
 
247
  print(f"Loading {model_name} on {device}...")
 
248
  try:
249
  if model_name == "27b-4a":
250
  # Use Gemma 4 27B in 4-bit (requires CUDA)
@@ -279,9 +284,11 @@ def load_model(model_name: str):
279
  print(f"Model {model_name} loaded successfully.")
280
  models[model_name] = model
281
  tokenizers[model_name] = tokenizer
 
282
  return model, tokenizer
283
  except Exception as e:
284
  print(f"Error loading model {model_name}: {e}")
 
285
  raise e
286
 
287
  # Pre-load default 2b
@@ -290,6 +297,10 @@ try:
290
  except:
291
  print("Could not preload 2b model.")
292
 
 
 
 
 
293
  class TextRequest(BaseModel):
294
  text: str
295
  layers: Optional[List[int]] = None # List of layer indices to average
@@ -297,11 +308,11 @@ class TextRequest(BaseModel):
297
  saliency_mode: str = "local" # "local" or "global"
298
 
299
  @app.post("/analyze")
300
- async def analyze_text_legacy(request: TextRequest):
301
- return await analyze_text_model("2b", request)
302
 
303
  @app.post("/analyze/{model_name}")
304
- async def analyze_text_model(model_name: str, request: TextRequest):
305
  text = request.text
306
  preprompt = request.preprompt.strip()
307
 
 
236
  # --- Saliency API (Existing) ---
237
  models = {}
238
  tokenizers = {}
239
+ model_status = {
240
+ "2b": "unloaded",
241
+ "27b-4a": "unloaded"
242
+ }
243
 
244
  device = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
245
  hf_token = os.environ.get("HF_TOKEN")
 
249
  return models[model_name], tokenizers[model_name]
250
 
251
  print(f"Loading {model_name} on {device}...")
252
+ model_status[model_name] = "downloading"
253
  try:
254
  if model_name == "27b-4a":
255
  # Use Gemma 4 27B in 4-bit (requires CUDA)
 
284
  print(f"Model {model_name} loaded successfully.")
285
  models[model_name] = model
286
  tokenizers[model_name] = tokenizer
287
+ model_status[model_name] = "loaded"
288
  return model, tokenizer
289
  except Exception as e:
290
  print(f"Error loading model {model_name}: {e}")
291
+ model_status[model_name] = "error"
292
  raise e
293
 
294
  # Pre-load default 2b
 
297
  except:
298
  print("Could not preload 2b model.")
299
 
300
+ @app.get("/status")
301
+ def get_model_status():
302
+ return model_status
303
+
304
  class TextRequest(BaseModel):
305
  text: str
306
  layers: Optional[List[int]] = None # List of layer indices to average
 
308
  saliency_mode: str = "local" # "local" or "global"
309
 
310
  @app.post("/analyze")
311
+ def analyze_text_legacy(request: TextRequest):
312
+ return analyze_text_model("2b", request)
313
 
314
  @app.post("/analyze/{model_name}")
315
+ def analyze_text_model(model_name: str, request: TextRequest):
316
  text = request.text
317
  preprompt = request.preprompt.strip()
318
 
static/index.html CHANGED
@@ -463,6 +463,7 @@
463
  const text = inputArea.value.trim();
464
  const preprompt = prepromptInput.value.trim();
465
  const saliencyMode = saliencyModeInput.value;
 
466
 
467
  const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
468
 
@@ -474,14 +475,34 @@
474
 
475
  analyzeBtn.disabled = true;
476
  loading.style.display = 'block';
 
477
  resultContainer.innerHTML = '';
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  try {
480
- const response = await fetch('/analyze', {
481
  method: 'POST',
482
  headers: { 'Content-Type': 'application/json' },
483
  body: JSON.stringify({ text, preprompt, layers: checkedLayers, saliency_mode: saliencyMode })
484
  });
 
 
485
 
486
  if (!response.ok) throw new Error('Network response was not ok');
487
 
@@ -783,7 +804,11 @@
783
 
784
  } catch (err) {
785
  console.error(err);
786
- alert("Error loading stats.");
 
 
 
 
787
  }
788
  }
789
  </script>
 
463
  const text = inputArea.value.trim();
464
  const preprompt = prepromptInput.value.trim();
465
  const saliencyMode = saliencyModeInput.value;
466
+ const modelVersion = "2b";
467
 
468
  const checkedLayers = Array.from(document.querySelectorAll('.layer-cb:checked')).map(cb => parseInt(cb.value));
469
 
 
475
 
476
  analyzeBtn.disabled = true;
477
  loading.style.display = 'block';
478
+ loading.textContent = 'Analyzing text with Gemma 4...';
479
  resultContainer.innerHTML = '';
480
 
481
+ let isFetching = true;
482
+ const pollStatus = async () => {
483
+ while(isFetching) {
484
+ try {
485
+ const statusRes = await fetch('/status');
486
+ if (statusRes.ok) {
487
+ const statusData = await statusRes.json();
488
+ if (statusData[modelVersion] === "downloading") {
489
+ loading.textContent = `Downloading Gemma 4 (${modelVersion}) Model... this takes a few minutes.`;
490
+ }
491
+ }
492
+ } catch(e) {}
493
+ await new Promise(r => setTimeout(r, 2000));
494
+ }
495
+ };
496
+ pollStatus();
497
+
498
  try {
499
+ const response = await fetch(`/analyze/${modelVersion}`, {
500
  method: 'POST',
501
  headers: { 'Content-Type': 'application/json' },
502
  body: JSON.stringify({ text, preprompt, layers: checkedLayers, saliency_mode: saliencyMode })
503
  });
504
+
505
+ isFetching = false;
506
 
507
  if (!response.ok) throw new Error('Network response was not ok');
508
 
 
804
 
805
  } catch (err) {
806
  console.error(err);
807
+ alert("Failed to analyze text.");
808
+ isFetching = false;
809
+ } finally {
810
+ analyzeBtn.disabled = false;
811
+ loading.style.display = 'none';
812
  }
813
  }
814
  </script>