Lucas ARRIESSE
commited on
Commit
·
4e54efb
1
Parent(s):
5e6193a
WIP
Browse files- app.py +83 -1
- index.html +53 -43
- static/script.js +29 -9
- 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
|
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 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
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 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
</
|
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 |
-
<
|
264 |
-
|
265 |
-
|
|
|
|
|
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="
|
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
|
588 |
-
|
589 |
-
|
590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
591 |
});
|
592 |
|
593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 |
+
}
|