Lucas ARRIESSE commited on
Commit
4e54efb
·
1 Parent(s): 5e6193a
Files changed (4) hide show
  1. app.py +83 -1
  2. index.html +53 -43
  3. static/script.js +29 -9
  4. static/sse.js +80 -0
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from bs4 import BeautifulSoup
2
  import warnings
3
  import io
@@ -14,7 +15,7 @@ from nltk.stem import WordNetLemmatizer
14
  from concurrent.futures import ThreadPoolExecutor, as_completed
15
  import json
16
  import traceback
17
- from fastapi import FastAPI, BackgroundTasks, HTTPException
18
  from fastapi.staticfiles import StaticFiles
19
  from schemas import *
20
  from fastapi.middleware.cors import CORSMiddleware
@@ -253,6 +254,8 @@ def get_meetings(req: MeetingsRequest):
253
 
254
  return MeetingsResponse(meetings=dict(zip(all_meetings, meeting_folders)))
255
 
 
 
256
 
257
  @app.post("/get_dataframe", response_model=DataResponse)
258
  def get_change_request_dataframe(req: DataRequest):
@@ -288,6 +291,8 @@ def get_change_request_dataframe(req: DataRequest):
288
  df = filtered_df.fillna("")
289
  return DataResponse(data=df[["TDoc", "Title", "Type", "TDoc Status", "Agenda item description", "URL"]].to_dict(orient="records"))
290
 
 
 
291
 
292
  @app.post("/download_tdocs")
293
  def download_tdocs(req: DownloadRequest):
@@ -337,6 +342,8 @@ def download_tdocs(req: DownloadRequest):
337
  media_type="application/zip"
338
  )
339
 
 
 
340
 
341
  @app.post("/generate_requirements", response_model=RequirementsResponse)
342
  async def gen_reqs(req: RequirementsRequest, background_tasks: BackgroundTasks):
@@ -411,6 +418,81 @@ async def gen_reqs(req: RequirementsRequest, background_tasks: BackgroundTasks):
411
  background_tasks.add_task(asyncio.sleep, 60)
412
  return RequirementsResponse(requirements=all_requirements)
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
  @app.post("/get_reqs_from_query", response_model=ReqSearchResponse)
416
  def find_requirements_from_problem_description(req: ReqSearchRequest):
 
1
+ from typing import Literal
2
  from bs4 import BeautifulSoup
3
  import warnings
4
  import io
 
15
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
  import json
17
  import traceback
18
+ from fastapi import FastAPI, BackgroundTasks, HTTPException, Request
19
  from fastapi.staticfiles import StaticFiles
20
  from schemas import *
21
  from fastapi.middleware.cors import CORSMiddleware
 
254
 
255
  return MeetingsResponse(meetings=dict(zip(all_meetings, meeting_folders)))
256
 
257
+ # ============================================================================================================================================
258
+
259
 
260
  @app.post("/get_dataframe", response_model=DataResponse)
261
  def get_change_request_dataframe(req: DataRequest):
 
291
  df = filtered_df.fillna("")
292
  return DataResponse(data=df[["TDoc", "Title", "Type", "TDoc Status", "Agenda item description", "URL"]].to_dict(orient="records"))
293
 
294
+ # ==================================================================================================================================
295
+
296
 
297
  @app.post("/download_tdocs")
298
  def download_tdocs(req: DownloadRequest):
 
342
  media_type="application/zip"
343
  )
344
 
345
+ # ========================================================================================================================
346
+
347
 
348
  @app.post("/generate_requirements", response_model=RequirementsResponse)
349
  async def gen_reqs(req: RequirementsRequest, background_tasks: BackgroundTasks):
 
418
  background_tasks.add_task(asyncio.sleep, 60)
419
  return RequirementsResponse(requirements=all_requirements)
420
 
421
+ # ======================================================================================================================================================================================
422
+
423
+ SUBPROCESS_SEMAPHORE = asyncio.Semaphore(32)
424
+
425
+
426
+ class ProgressUpdate(BaseModel):
427
+ """Defines the structure of a single SSE message."""
428
+ status: Literal["progress", "complete"]
429
+ data: dict
430
+ total_docs: int
431
+ processed_docs: int
432
+
433
+
434
+ @app.post("/generate_requirements/v2")
435
+ async def gen_reqs(req: RequirementsRequest, con: Request):
436
+ """Extract requirements from the specified TDocs using a LLM and returns SSE events about the progress of ongoing operations"""
437
+
438
+ documents = req.documents
439
+ n_docs = len(documents)
440
+
441
+ logging.info("Generating requirements for documents: {}".format(
442
+ [doc.document for doc in documents]))
443
+
444
+ def prompt(doc_id, full):
445
+ return f"Here's the document whose ID is {doc_id} : {full}\n\nExtract all requirements and group them by context, returning a list of objects where each object includes a document ID, a concise description of the context where the requirements apply (not a chapter title or copied text), and a list of associated requirements; always return the result as a list, even if only one context is found. Remove the errors"
446
+
447
+ async def _process_document(doc) -> list[DocRequirements]:
448
+ doc_id = doc.document
449
+ url = doc.url
450
+
451
+ # convert the docx to txt for use
452
+ try:
453
+ full = "\n".join(docx_to_txt(doc_id, url))
454
+ except Exception as e:
455
+ traceback.print_exception(e)
456
+ return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
457
+
458
+ try:
459
+ model_used = "gemini-v2"
460
+ resp_ai = await llm_router.acompletion(
461
+ model=model_used,
462
+ messages=[
463
+ {"role": "user", "content": prompt(doc_id, full)}],
464
+ response_format=RequirementsResponse
465
+ )
466
+ return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
467
+ except Exception as e:
468
+ return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
469
+
470
+ # futures for all processed documents
471
+ process_futures = [_process_document(doc) for doc in documents]
472
+
473
+ # lambda to print progress
474
+ def progress_update(x): return f"data: {x.model_dump_json()}\n\n"
475
+
476
+ # async generator that generates the SSE events for progress
477
+ async def _stream_generator(docs: list[asyncio.Future]):
478
+ items = []
479
+ n_processed = 0
480
+
481
+ yield progress_update(ProgressUpdate(status="progress", data={}, total_docs=n_docs, processed_docs=0))
482
+
483
+ for doc in asyncio.as_completed(docs):
484
+ result = await doc
485
+ items.extend(result)
486
+ n_processed += 1
487
+ yield progress_update(ProgressUpdate(status="progress", data={}, total_docs=n_docs, processed_docs=n_processed))
488
+
489
+ final_response = RequirementsResponse(requirements=items)
490
+
491
+ yield progress_update(ProgressUpdate(status="complete", data=final_response.model_dump(), total_docs=n_docs, processed_docs=n_processed))
492
+
493
+ return StreamingResponse(_stream_generator(process_futures), media_type="text/event-stream")
494
+ # =======================================================================================================================================================================================
495
+
496
 
497
  @app.post("/get_reqs_from_query", response_model=ReqSearchResponse)
498
  def find_requirements_from_problem_description(req: ReqSearchRequest):
index.html CHANGED
@@ -11,58 +11,66 @@
11
 
12
  <body class="bg-gray-100 min-h-screen">
13
  <!-- Loading Overlay -->
14
- <div id="loading-overlay" class="fixed inset-0 bg-black opacity-50 flex items-center justify-center z-50 hidden">
15
  <div class="bg-white p-6 rounded-lg shadow-lg text-center">
16
- <div id="loading-bar" class="w-64 h-4 bg-gray-200 rounded-full mb-4">
17
  <div class="h-full bg-blue-500 rounded-full animate-pulse"></div>
18
- </div>
 
 
19
  <p id="progress-text" class="text-gray-700">Chargement en cours...</p>
20
  </div>
21
  </div>
22
 
23
  <div class="container mx-auto p-6">
24
  <h1 class="text-3xl font-bold text-center mb-8">Requirements Extractor</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- <!-- Working Group Selection -->
27
- <div id="working-group-container" class="mb-6">
28
- <label for="working-group-select" class="block text-sm font-medium text-gray-700 mb-2">Working Group</label>
29
- <select id="working-group-select" class="w-full p-2 border border-gray-300 rounded-md">
30
- <option value="">Select a working group</option>
31
- <option value="SA1">SA1</option>
32
- <option value="SA2">SA2</option>
33
- <option value="SA3">SA3</option>
34
- <option value="SA4">SA4</option>
35
- <option value="SA5">SA5</option>
36
- <option value="SA6">SA6</option>
37
- <option value="CT1">CT1</option>
38
- <option value="CT2">CT2</option>
39
- <option value="CT3">CT3</option>
40
- <option value="CT4">CT4</option>
41
- <option value="CT5">CT5</option>
42
- <option value="CT6">CT6</option>
43
- </select>
44
- <button id="get-meetings-btn" class="mt-2 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
45
- Get Meetings
46
- </button>
47
- </div>
48
 
49
- <!-- Meeting Selection -->
50
- <div id="meeting-container" class="mb-6 hidden">
51
- <label for="meeting-select" class="block text-sm font-medium text-gray-700 mb-2">Meeting</label>
52
- <select id="meeting-select" class="w-full p-2 border border-gray-300 rounded-md">
53
- <option value="">Select a meeting</option>
54
- </select>
55
- <button id="get-tdocs-btn" class="mt-2 px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">
56
- Get TDocs
57
- </button>
58
  </div>
59
 
60
-
61
-
62
  <!-- Add an horizontal separation -->
63
  <hr>
64
  <!-- Tab list for subsections -->
65
- <div role="tablist" class="tabs tabs-border" id="tab-container">
66
  <a role="tab" class="tab tab-active" id="doc-table-tab" onclick="switchTab('doc-table-tab')">📝
67
  Documents</a>
68
  <a role="tab" class="tab tab-disabled" id="requirements-tab" onclick="switchTab('requirements-tab')">
@@ -242,8 +250,7 @@
242
  <label class="label">
243
  <span class="label-text text-base-content">Number of categories</span>
244
  </label>
245
- <input type="checkbox" class="toggle toggle-primary" id="auto-detect-toggle"
246
- onchange="toggleAutoDetect(this)" />
247
  <div class="text-xs mt-1 text-base-content">Manual | Auto detect</div>
248
  </div>
249
 
@@ -260,9 +267,11 @@
260
  </div>
261
  <div class="justify-end pl-5">
262
  <!--Copy reqs button-->
263
- <button class="btn btn-square" id="copy-reqs-btn" aria-label="Copy">
264
- 📋
265
- </button>
 
 
266
  </div>
267
  </div>
268
  <div id="categorized-requirements-list"></div>
@@ -272,7 +281,7 @@
272
  <div id="solutions-action-buttons-container" class="mb-6 hidden">
273
  <div class="flex flex-wrap gap-2 justify-center items-center">
274
  <div class="join">
275
- <input id="category-count" type="number" class="input join-item w-24" min="1" value="3" />
276
  <button id="get-solutions-btn"
277
  class="btn join-item px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600">
278
  Run multiple steps
@@ -304,6 +313,7 @@
304
  </div>
305
  </div>
306
 
 
307
  <script src="/static/script.js"></script>
308
  </body>
309
 
 
11
 
12
  <body class="bg-gray-100 min-h-screen">
13
  <!-- Loading Overlay -->
14
+ <div id="loading-overlay" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
15
  <div class="bg-white p-6 rounded-lg shadow-lg text-center">
16
+ <!-- <div id="loading-bar" class="w-64 h-4 bg-gray-200 rounded-full mb-4">
17
  <div class="h-full bg-blue-500 rounded-full animate-pulse"></div>
18
+ </div> -->
19
+
20
+ <span class="loading loading-spinner loading-xl"></span>
21
  <p id="progress-text" class="text-gray-700">Chargement en cours...</p>
22
  </div>
23
  </div>
24
 
25
  <div class="container mx-auto p-6">
26
  <h1 class="text-3xl font-bold text-center mb-8">Requirements Extractor</h1>
27
+ <div id="selection-container" class="mb-6">
28
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
29
+ <!-- Working Group -->
30
+ <div>
31
+ <label for="working-group-select" class="block text-sm font-medium text-gray-700 mb-2">Working
32
+ Group</label>
33
+ <select id="working-group-select" class="w-full p-2 border border-gray-300 rounded-md">
34
+ <option value="" selected>Select a working group</option>
35
+ <option value="SA1">SA1</option>
36
+ <option value="SA2">SA2</option>
37
+ <option value="SA3">SA3</option>
38
+ <option value="SA4">SA4</option>
39
+ <option value="SA5">SA5</option>
40
+ <option value="SA6">SA6</option>
41
+ <option value="CT1">CT1</option>
42
+ <option value="CT2">CT2</option>
43
+ <option value="CT3">CT3</option>
44
+ <option value="CT4">CT4</option>
45
+ <option value="CT5">CT5</option>
46
+ <option value="CT6">CT6</option>
47
+ </select>
48
+ </div>
49
 
50
+ <!-- Meeting -->
51
+ <div>
52
+ <label for="meeting-select" class="block text-sm font-medium text-gray-700 mb-2">Meeting</label>
53
+ <select id="meeting-select" class="w-full p-2 border border-gray-300 rounded-md">
54
+ <option value="">Select a meeting</option>
55
+ </select>
56
+ </div>
57
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ <!-- Buttons -->
60
+ <div class="flex flex-col sm:flex-row gap-2">
61
+ <!-- <button id="get-meetings-btn" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
62
+ Get Meetings
63
+ </button> -->
64
+ <button id="get-tdocs-btn" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">
65
+ Get TDocs
66
+ </button>
67
+ </div>
68
  </div>
69
 
 
 
70
  <!-- Add an horizontal separation -->
71
  <hr>
72
  <!-- Tab list for subsections -->
73
+ <div role="tablist" class="tabs tabs-border tabs-xl" id="tab-container">
74
  <a role="tab" class="tab tab-active" id="doc-table-tab" onclick="switchTab('doc-table-tab')">📝
75
  Documents</a>
76
  <a role="tab" class="tab tab-disabled" id="requirements-tab" onclick="switchTab('requirements-tab')">
 
250
  <label class="label">
251
  <span class="label-text text-base-content">Number of categories</span>
252
  </label>
253
+ <input type="checkbox" class="toggle toggle-primary" id="auto-detect-toggle" />
 
254
  <div class="text-xs mt-1 text-base-content">Manual | Auto detect</div>
255
  </div>
256
 
 
267
  </div>
268
  <div class="justify-end pl-5">
269
  <!--Copy reqs button-->
270
+ <div class="tooltip" data-tip="Copy selected requirements to clipboard">
271
+ <button class="btn btn-square" id="copy-reqs-btn" aria-label="Copy">
272
+ 📋
273
+ </button>
274
+ </div>
275
  </div>
276
  </div>
277
  <div id="categorized-requirements-list"></div>
 
281
  <div id="solutions-action-buttons-container" class="mb-6 hidden">
282
  <div class="flex flex-wrap gap-2 justify-center items-center">
283
  <div class="join">
284
+ <input id="solution-gen-nsteps" type="number" class="input join-item w-24" min="1" value="3" />
285
  <button id="get-solutions-btn"
286
  class="btn join-item px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600">
287
  Run multiple steps
 
313
  </div>
314
  </div>
315
 
316
+ <script src="/static/sse.js"></script>
317
  <script src="/static/script.js"></script>
318
  </body>
319
 
static/script.js CHANGED
@@ -1,3 +1,4 @@
 
1
  // ==================================== Variables globales ========================================
2
  let requirements = [];
3
 
@@ -584,13 +585,27 @@ async function extractRequirements() {
584
  toggleElementsEnabled(['extract-requirements-btn'], false);
585
 
586
  try {
587
- const response = await fetch('/generate_requirements', {
588
- method: 'POST',
589
- headers: { 'Content-Type': 'application/json' },
590
- body: JSON.stringify({ documents: selectedData })
 
 
 
 
 
 
 
591
  });
592
 
593
- const data = await response.json();
 
 
 
 
 
 
 
594
  requirements = data.requirements;
595
  let req_id = 0;
596
  data.requirements.forEach(obj => {
@@ -604,6 +619,7 @@ async function extractRequirements() {
604
  req_id++;
605
  })
606
  })
 
607
  displayRequirements(requirements);
608
 
609
  toggleContainersVisibility(['requirements-container', 'query-requirements-container'], true);
@@ -773,7 +789,7 @@ function copySelectedRequirementsAsMarkdown() {
773
  selected.categories.forEach(category => {
774
  lines.push(`### ${category.title}`);
775
  category.requirements.forEach(req => {
776
- lines.push(`- ${req.requirement}`);
777
  });
778
  lines.push(''); // Add an empty line after each category
779
  });
@@ -782,7 +798,7 @@ function copySelectedRequirementsAsMarkdown() {
782
 
783
  navigator.clipboard.writeText(markdownText).then(() => {
784
  console.log("Markdown copied to clipboard.");
785
- alert("Selected requirements copied to clipboard");
786
  }).catch(err => {
787
  console.error("Failed to copy markdown:", err);
788
  });
@@ -1282,7 +1298,10 @@ async function workflow(steps = 1) {
1282
 
1283
  document.addEventListener('DOMContentLoaded', function () {
1284
  // Événements des boutons principaux
1285
- document.getElementById('get-meetings-btn').addEventListener('click', getMeetings);
 
 
 
1286
  document.getElementById('get-tdocs-btn').addEventListener('click', getTDocs);
1287
  document.getElementById('download-tdocs-btn').addEventListener('click', downloadTDocs);
1288
  document.getElementById('extract-requirements-btn').addEventListener('click', extractRequirements);
@@ -1297,7 +1316,8 @@ document.addEventListener('DOMContentLoaded', function () {
1297
 
1298
  // Événements pour les boutons de solutions (à implémenter plus tard)
1299
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1300
- alert('Fonctionnalité à implémenter');
 
1301
  });
1302
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1303
  workflow();
 
1
+
2
  // ==================================== Variables globales ========================================
3
  let requirements = [];
4
 
 
585
  toggleElementsEnabled(['extract-requirements-btn'], false);
586
 
587
  try {
588
+ const response = await postWithSSE('/generate_requirements/v2', { documents: selectedData }, {
589
+ onMessage: (msg) => {
590
+ console.log("SSE message:");
591
+ console.log(msg);
592
+
593
+ showLoadingOverlay(`Extraction des requirements en cours... (${msg.processed_docs}/${msg.total_docs})`);
594
+ },
595
+ onError: (err) => {
596
+ console.error(`Error while fetching requirements: ${err}`);
597
+ throw err;
598
+ }
599
  });
600
 
601
+
602
+ // const response = await fetch('/generate_requirements/', {
603
+ // method: 'POST',
604
+ // headers: { 'Content-Type': 'application/json' },
605
+ // body: req
606
+ // });
607
+
608
+ const data = response.data; // data in the SSE message contains the requirements response
609
  requirements = data.requirements;
610
  let req_id = 0;
611
  data.requirements.forEach(obj => {
 
619
  req_id++;
620
  })
621
  })
622
+
623
  displayRequirements(requirements);
624
 
625
  toggleContainersVisibility(['requirements-container', 'query-requirements-container'], true);
 
789
  selected.categories.forEach(category => {
790
  lines.push(`### ${category.title}`);
791
  category.requirements.forEach(req => {
792
+ lines.push(`- ${req.requirement} (${req.document})`);
793
  });
794
  lines.push(''); // Add an empty line after each category
795
  });
 
798
 
799
  navigator.clipboard.writeText(markdownText).then(() => {
800
  console.log("Markdown copied to clipboard.");
801
+ alert("Selected requirements copied to clipboard");
802
  }).catch(err => {
803
  console.error("Failed to copy markdown:", err);
804
  });
 
1298
 
1299
  document.addEventListener('DOMContentLoaded', function () {
1300
  // Événements des boutons principaux
1301
+ // document.getElementById('get-meetings-btn').addEventListener('click', getMeetings);
1302
+ document.getElementById('working-group-select').addEventListener('change', (ev) => {
1303
+ getMeetings();
1304
+ });
1305
  document.getElementById('get-tdocs-btn').addEventListener('click', getTDocs);
1306
  document.getElementById('download-tdocs-btn').addEventListener('click', downloadTDocs);
1307
  document.getElementById('extract-requirements-btn').addEventListener('click', extractRequirements);
 
1316
 
1317
  // Événements pour les boutons de solutions (à implémenter plus tard)
1318
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1319
+ const n_steps = document.getElementById('solution-gen-nsteps').value;
1320
+ workflow(n_steps);
1321
  });
1322
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1323
  workflow();
static/sse.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // sse-fetch.js
2
+
3
+ /**
4
+ * Performs a POST request and handles the response as a Server-Sent Events (SSE) stream.
5
+ * The standard EventSource API does not support POST requests, so we use fetch.
6
+ *
7
+ * @param {string} url The URL to send the POST request to.
8
+ * @param {object} body The JSON body for the POST request.
9
+ * @param {object} callbacks An object containing callback functions.
10
+ * @param {(data: object) => void} callbacks.onMessage A function called for each message received.
11
+ * @param {(error: Error) => void} callbacks.onError A function called if an error occurs.
12
+ */
13
+ async function postWithSSE(url, body, callbacks) {
14
+ const { onMessage, onError } = callbacks;
15
+
16
+ try {
17
+ const response = await fetch(url, {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ 'Accept': 'text/event-stream' // Politely ask for an event stream
22
+ },
23
+ body: JSON.stringify(body),
24
+ });
25
+
26
+ if (!response.ok) {
27
+ throw new Error(`HTTP error! status: ${response.status}`);
28
+ }
29
+
30
+ if (!response.body) {
31
+ throw new Error('Response body is null.');
32
+ }
33
+
34
+ const reader = response.body.getReader();
35
+ const decoder = new TextDecoder();
36
+ let buffer = '';
37
+
38
+ while (true) {
39
+ const { value, done } = await reader.read();
40
+
41
+ // Decode the chunk of data and add it to our buffer.
42
+ // The `stream: true` option is important for multi-byte characters.
43
+ const chunk = decoder.decode(value, { stream: true });
44
+ buffer += chunk;
45
+
46
+ // SSE messages are separated by double newlines (`\n\n`).
47
+ // A single chunk from the stream might contain multiple messages or a partial message.
48
+ // We process all complete messages in the buffer.
49
+ let boundary;
50
+ while ((boundary = buffer.indexOf('\n\n')) !== -1) {
51
+ const messageString = buffer.substring(0, boundary);
52
+ buffer = buffer.substring(boundary + 2); // Remove the processed message from the buffer
53
+
54
+ // Skip empty keep-alive messages
55
+ if (messageString.trim() === '') {
56
+ continue;
57
+ }
58
+
59
+ // SSE "data:" lines. Your server only uses `data:`.
60
+ // We remove the "data: " prefix to get the JSON payload.
61
+ if (messageString.startsWith('data:')) {
62
+ const jsonData = messageString.substring('data: '.length);
63
+ try {
64
+ const parsedData = JSON.parse(jsonData);
65
+ if (parsedData.status === "complete")
66
+ return parsedData;
67
+ else
68
+ onMessage(parsedData);
69
+ } catch (e) {
70
+ console.error("Failed to parse JSON from SSE message:", jsonData, e);
71
+ // Optionally call the onError callback for parsing errors
72
+ if (onError) onError(new Error("Failed to parse JSON from SSE message."));
73
+ }
74
+ }
75
+ }
76
+ }
77
+ } catch (error) {
78
+ throw error;
79
+ }
80
+ }