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 |
+
}
|