om4r932 commited on
Commit
040cfa1
·
1 Parent(s): 1b96641

Add query requirements capacity + TODO : Categorization

Browse files
Files changed (2) hide show
  1. app.py +136 -52
  2. index.html +111 -18
app.py CHANGED
@@ -1,62 +1,68 @@
1
- from fastapi import FastAPI
 
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import FileResponse
4
- import litellm
 
5
  import pandas as pd
6
- from pydantic import BaseModel, Field
7
- from typing import Any, List, Dict, Optional
8
  import re
 
 
 
 
 
 
 
 
 
 
 
9
  import subprocess
10
  import requests
 
 
 
 
11
  import os
12
  from lxml import etree
13
  import zipfile
14
  import io
15
  import warnings
 
16
  warnings.filterwarnings("ignore")
 
17
  from bs4 import BeautifulSoup
18
 
19
  app = FastAPI(title="Requirements Extractor")
20
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_methods=["*"], allow_origins=["*"])
 
 
 
21
 
22
- class MeetingsRequest(BaseModel):
23
- working_group: str
24
-
25
- class MeetingsResponse(BaseModel):
26
- meetings: Dict[str, str]
27
-
28
- class DataRequest(BaseModel):
29
- working_group: str
30
- meeting: str
31
-
32
- class DataResponse(BaseModel):
33
- data: List[Dict[Any, Any]]
34
-
35
- class DocRequirements(BaseModel):
36
- doc_id: str
37
- context: str
38
- requirements: List[str]
39
-
40
- class DocInfo(BaseModel):
41
- document: str
42
- url: str
43
-
44
- class RequirementsRequest(BaseModel):
45
- documents: List[DocInfo]
46
-
47
- class RequirementsResponse(BaseModel):
48
- requirements: List[DocRequirements]
49
 
50
  NSMAP = {
51
  'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
52
  'v': 'urn:schemas-microsoft-com:vml'
53
  }
54
 
 
 
 
 
 
 
55
  def get_docx_archive(url: str) -> zipfile.ZipFile:
56
  """Récupère le docx depuis l'URL et le retourne comme objet ZipFile"""
57
  if not url.endswith("zip"):
58
  raise ValueError("URL doit pointer vers un fichier ZIP")
59
-
60
  resp = requests.get(url, verify=False, headers={
61
  "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
62
  })
@@ -64,10 +70,33 @@ def get_docx_archive(url: str) -> zipfile.ZipFile:
64
 
65
  with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
66
  for file_name in zf.namelist():
67
- if file_name.endswith((".docx", ".doc")):
68
  docx_bytes = zf.read(file_name)
69
  return zipfile.ZipFile(io.BytesIO(docx_bytes))
70
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  raise ValueError("Aucun fichier docx/doc trouvé dans l'archive")
72
 
73
  def parse_document_xml(docx_zip: zipfile.ZipFile) -> etree._ElementTree:
@@ -210,25 +239,80 @@ def get_change_request_dataframe(req: DataRequest):
210
  return DataResponse(data=df[["TDoc", "Title", "Type", "TDoc Status", "Agenda item description", "URL"]].to_dict(orient="records"))
211
 
212
  @app.post("/generate_requirements", response_model=RequirementsResponse)
213
- def gen_reqs(req: RequirementsRequest):
214
  documents = req.documents
215
- output = []
216
- for doc in documents:
 
217
  doc_id = doc.document
218
  url = doc.url
219
-
220
- full = "\n".join(docx_to_txt(doc_id, url))
221
-
222
- resp_ai = litellm.completion(
223
- model="gemini/gemini-2.0-flash",
224
- api_key=os.environ.get("GEMINI"),
225
- messages=[{"role":"user","content": f"Here's the document whose ID is {doc_id} with requirements : {full}\n\nI want you to extract all the requirements and give me a context (not giving the section or whatever, a sentence is needed) where that calls for those requirements. If multiples covered contexts is present, make as many requirements list by context as you want."}],
226
- response_format=DocRequirements
227
- )
228
-
229
- reqs = DocRequirements.model_validate_json(resp_ai.choices[0].message.content)
230
- output.append(reqs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- return RequirementsResponse(requirements=output)
233
-
234
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import traceback
2
+ from fastapi import FastAPI, BackgroundTasks
3
+ from schemas import *
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.responses import FileResponse
6
+ from litellm.router import Router
7
+ from aiolimiter import AsyncLimiter
8
  import pandas as pd
9
+ import asyncio
 
10
  import re
11
+ import nltk
12
+
13
+ nltk.download('stopwords')
14
+ nltk.download('punkt_tab')
15
+ nltk.download('wordnet')
16
+
17
+ from nltk.stem import WordNetLemmatizer
18
+ from nltk.corpus import stopwords
19
+ from nltk.tokenize import word_tokenize
20
+
21
+ import string
22
  import subprocess
23
  import requests
24
+ from dotenv import load_dotenv
25
+
26
+ load_dotenv()
27
+
28
  import os
29
  from lxml import etree
30
  import zipfile
31
  import io
32
  import warnings
33
+
34
  warnings.filterwarnings("ignore")
35
+
36
  from bs4 import BeautifulSoup
37
 
38
  app = FastAPI(title="Requirements Extractor")
39
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_methods=["*"], allow_origins=["*"])
40
+ llm_router = Router(model_list=[{"model_name": "gemini-v1", "litellm_params": {"model": "gemini/gemini-2.0-flash", "api_key": os.environ.get("GEMINI"), "max_retries": 10, "rpm": 15}},
41
+ {"model_name": "gemini-v2", "litellm_params": {"model": "gemini/gemini-2.5-flash", "api_key": os.environ.get("GEMINI"), "max_retries": 10, "rpm": 10}}]
42
+ , fallbacks=[{"gemini-v2": ["gemini-v1"]}], num_retries=10)
43
 
44
+ limiter_mapping = {
45
+ model["model_name"]: AsyncLimiter(model["litellm_params"]["rpm"], 60)
46
+ for model in llm_router.model_list
47
+ }
48
+ lemmatizer = WordNetLemmatizer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  NSMAP = {
51
  'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
52
  'v': 'urn:schemas-microsoft-com:vml'
53
  }
54
 
55
+ def lemma(text: str):
56
+ stop_words = set(stopwords.words('english'))
57
+ txt = text.translate(str.maketrans('', '', string.punctuation)).strip()
58
+ tokens = [token for token in word_tokenize(txt.lower()) if token not in stop_words]
59
+ return [lemmatizer.lemmatize(token) for token in tokens]
60
+
61
  def get_docx_archive(url: str) -> zipfile.ZipFile:
62
  """Récupère le docx depuis l'URL et le retourne comme objet ZipFile"""
63
  if not url.endswith("zip"):
64
  raise ValueError("URL doit pointer vers un fichier ZIP")
65
+ doc_id = os.path.splitext(os.path.basename(url))[0]
66
  resp = requests.get(url, verify=False, headers={
67
  "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
68
  })
 
70
 
71
  with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
72
  for file_name in zf.namelist():
73
+ if file_name.endswith(".docx"):
74
  docx_bytes = zf.read(file_name)
75
  return zipfile.ZipFile(io.BytesIO(docx_bytes))
76
+ elif file_name.endswith(".doc"):
77
+ input_path = f"/tmp/{doc_id}.doc"
78
+ output_path = f"/tmp/{doc_id}.docx"
79
+ docx_bytes = zf.read(file_name)
80
+
81
+ with open(input_path, "wb") as f:
82
+ f.write(docx_bytes)
83
+
84
+ subprocess.run([
85
+ "libreoffice",
86
+ "--headless",
87
+ "--convert-to", "docx",
88
+ "--outdir", "/tmp",
89
+ input_path
90
+ ], check=True)
91
+
92
+ with open(output_path, "rb") as f:
93
+ docx_bytes = f.read()
94
+
95
+ os.remove(input_path)
96
+ os.remove(output_path)
97
+
98
+ return zipfile.ZipFile(io.BytesIO(docx_bytes))
99
+
100
  raise ValueError("Aucun fichier docx/doc trouvé dans l'archive")
101
 
102
  def parse_document_xml(docx_zip: zipfile.ZipFile) -> etree._ElementTree:
 
239
  return DataResponse(data=df[["TDoc", "Title", "Type", "TDoc Status", "Agenda item description", "URL"]].to_dict(orient="records"))
240
 
241
  @app.post("/generate_requirements", response_model=RequirementsResponse)
242
+ async def gen_reqs(req: RequirementsRequest, background_tasks: BackgroundTasks):
243
  documents = req.documents
244
+ n_docs = len(documents)
245
+
246
+ async def process_document(doc):
247
  doc_id = doc.document
248
  url = doc.url
249
+ try:
250
+ full = "\n".join(docx_to_txt(doc_id, url))
251
+ except Exception as e:
252
+ traceback.print_exception(e)
253
+ return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
254
+
255
+ try:
256
+ model_used = "gemini-v2" # À adapter si fallback activé
257
+ async with limiter_mapping[model_used]:
258
+ resp_ai = await llm_router.acompletion(
259
+ model=model_used,
260
+ messages=[{"role":"user","content": 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."}],
261
+ response_format=RequirementsResponse
262
+ )
263
+ return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
264
+ except Exception as e:
265
+ if "rate limit" in str(e).lower():
266
+ try:
267
+ model_used = "gemini-v2" # À adapter si fallback activé
268
+ async with limiter_mapping[model_used]:
269
+ resp_ai = await llm_router.acompletion(
270
+ model=model_used,
271
+ messages=[{"role":"user","content": 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."}],
272
+ response_format=RequirementsResponse
273
+ )
274
+ return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
275
+ except Exception as fallback_e:
276
+ traceback.print_exception(fallback_e)
277
+ return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
278
+ else:
279
+ traceback.print_exception(e)
280
+ return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
281
 
282
+ async def process_batch(batch):
283
+ results = await asyncio.gather(*(process_document(doc) for doc in batch))
284
+ return [item for sublist in results for item in sublist]
285
+
286
+ all_requirements = []
287
+
288
+ if n_docs <= 30:
289
+ batch_results = await process_batch(documents)
290
+ all_requirements.extend(batch_results)
291
+ else:
292
+ batch_size = 30
293
+ batches = [documents[i:i + batch_size] for i in range(0, n_docs, batch_size)]
294
+
295
+ for i, batch in enumerate(batches):
296
+ batch_results = await process_batch(batch)
297
+ all_requirements.extend(batch_results)
298
+
299
+ if i < len(batches) - 1:
300
+ background_tasks.add_task(asyncio.sleep, 60)
301
+ return RequirementsResponse(requirements=all_requirements)
302
+
303
+ @app.post("/get_reqs_from_query", response_model=ReqSearchResponse)
304
+ def find_requirements_from_problem_description(req: ReqSearchRequest):
305
+ requirements = req.requirements
306
+ query = req.query
307
+
308
+ requirements_text = "\n".join([f"[Document: {r.document} | Context: {r.context} | Requirement: {r.requirement}]" for r in requirements])
309
+
310
+ print("Called the LLM")
311
+ resp_ai = llm_router.completion(
312
+ model="gemini-v2",
313
+ messages=[{"role":"user","content": f"Given all the requirements : \n {requirements_text} \n and the problem description \"{query}\", return a list of objects each with document ID, context, and requirement for the most relevant requirements that reference or best cover the problem."}],
314
+ response_format=ReqSearchResponse
315
+ )
316
+ print("Answered")
317
+
318
+ return ReqSearchResponse.model_validate_json(resp_ai.choices[0].message.content)
index.html CHANGED
@@ -10,7 +10,7 @@
10
  <body class="p-8 bg-base-100">
11
  <div class="container mx-auto">
12
  <h1 class="text-4xl font-bold text-center mb-8">Requirements Extractor</h1>
13
- <div>
14
  <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
15
  <select class="select select-bordered" id="workingGroupSelect">
16
  <option disabled selected value="">Working Group</option>
@@ -39,22 +39,38 @@
39
  <option disabled selected value="">Type</option>
40
  <option>Tous</option>
41
  </select>
42
-
43
  <select class="select select-bordered" id="docStatus">
44
  <option disabled selected value="">Status</option>
45
  <option>Tous</option>
46
  </select>
47
-
48
  <select class="select select-bordered" id="agendaItem">
49
- <option disabled selected value = "">Agenda</option>
50
  <option>Tous</option>
51
  </select>
52
  </div>
53
  </div>
54
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  <!-- Tableau des données -->
57
- <div class="max-h-[65vh] overflow-y-auto">
58
  <table class="table table-zebra w-full" id="dataFrame">
59
  <thead class="sticky top-0 bg-base-200 z-10">
60
  <tr class="bg-base-200">
@@ -71,11 +87,23 @@
71
  </table>
72
  </div>
73
 
74
- <center><button class="btn mt-6 gap-4" id="getReqs">Get Requirements</button></center>
 
 
 
 
 
 
 
 
 
75
  </div>
76
 
77
  <script>
 
 
78
  function getDataFrame(){
 
79
  const wg = document.getElementById('workingGroupSelect').value;
80
  const meeting = document.getElementById('meetingSelect').value;
81
  document.getElementById('docType').innerHTML = `
@@ -84,21 +112,23 @@
84
  `
85
 
86
  document.getElementById('docStatus').innerHTML = `
87
- <option disabled selected value="">Type</option>
88
  <option>Tous</option>
89
  `
90
 
91
  document.getElementById('agendaItem').innerHTML = `
92
- <option disabled selected value="">Type</option>
93
  <option>Tous</option>
94
  `
95
  const dataFrame = document.getElementById("dataFrame");
96
- document.getElementById("getTDocs").setAttribute('disabled', 'true')
97
- document.getElementById("getTDocs").innerHTML = "Loading ...";
 
98
  fetch("/get_dataframe", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({"working_group": wg, "meeting": meeting})})
99
  .then(resp => resp.json())
100
  .then(data => {
101
  document.getElementById("filters").classList.remove("hidden")
 
102
  const dataframeBody = dataFrame.querySelector("tbody");
103
  dataframeBody.innerHTML = "";
104
  const setType = new Set();
@@ -147,8 +177,8 @@
147
  })
148
  })
149
 
150
- document.getElementById("getTDocs").removeAttribute("disabled")
151
- document.getElementById("getTDocs").innerHTML = "Get TDocs";
152
  }
153
 
154
  function filterTable() {
@@ -186,18 +216,72 @@
186
  })
187
  }
188
 
189
- function tableToGenBody(tableSelector) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  // columnsMap : { "NomHeaderDansTable": "nom_voulu", ... }
191
- let columnsMap = {"TDoc": "doc_id", "URL": "url"};
192
- const table = document.querySelector(tableSelector);
193
- const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.innerText.trim());
194
 
195
  // Indices des colonnes à extraire
196
  const selectedIndices = headers
197
  .map((header, idx) => columnsMap[header] ? idx : -1)
198
  .filter(idx => idx !== -1);
199
 
200
- return Array.from(table.querySelectorAll('tbody tr'))
201
  .filter(row => getComputedStyle(row).display !== 'none')
202
  .map(row => {
203
  const cells = Array.from(row.querySelectorAll('td'));
@@ -218,6 +302,15 @@
218
  document.getElementById('agendaItem').addEventListener('change', filterTable)
219
  document.getElementById("workingGroupSelect").addEventListener('change', getMeetings)
220
  document.getElementById('getTDocs').addEventListener('click', getDataFrame)
 
 
 
 
 
 
 
 
 
221
  </script>
222
  </body>
223
  </html>
 
10
  <body class="p-8 bg-base-100">
11
  <div class="container mx-auto">
12
  <h1 class="text-4xl font-bold text-center mb-8">Requirements Extractor</h1>
13
+ <div id="dataFrameForm">
14
  <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
15
  <select class="select select-bordered" id="workingGroupSelect">
16
  <option disabled selected value="">Working Group</option>
 
39
  <option disabled selected value="">Type</option>
40
  <option>Tous</option>
41
  </select>
42
+
43
  <select class="select select-bordered" id="docStatus">
44
  <option disabled selected value="">Status</option>
45
  <option>Tous</option>
46
  </select>
47
+
48
  <select class="select select-bordered" id="agendaItem">
49
+ <option disabled selected value="">Agenda Item</option>
50
  <option>Tous</option>
51
  </select>
52
  </div>
53
  </div>
54
+
55
+ <div class="flex justify-center mt-12 min-h-screen hidden" id="queryReqForm">
56
+ <div class="w-full max-w-md">
57
+ <div class="grid grid-cols-1 gap-4">
58
+ <textarea placeholder="Enter your problem description here ..."
59
+ class="w-full mx-auto px-4 py-2 border rounded" id="problemDescription" />
60
+ <button class="w-1/2 mx-auto px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" id="queryReq">
61
+ Find requirements
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <center>
68
+ <span class="loading loading-bars loading-xl hidden" id="loadingBar"></span>
69
+ <p class="hidden" id="progressText"></p>
70
+ </center>
71
 
72
  <!-- Tableau des données -->
73
+ <div class="max-h-[65vh] overflow-y-auto" id="dataFrameDiv">
74
  <table class="table table-zebra w-full" id="dataFrame">
75
  <thead class="sticky top-0 bg-base-200 z-10">
76
  <tr class="bg-base-200">
 
87
  </table>
88
  </div>
89
 
90
+ <center>
91
+ <div id="buttons">
92
+ <p id="reqStatus" class="mt-6 hidden">Requirements extracted</p>
93
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
94
+ <button class="btn mt-6" id="getReqs">Get Requirements</button>
95
+ <button class="btn mt-6 hidden" id="searchReq">Query requirements</button>
96
+ <button class="btn mt-6 hidden" id="categorizeReq">Categorize requirements</button>
97
+ </div>
98
+ </div>
99
+ </center>
100
  </div>
101
 
102
  <script>
103
+ let requirements;
104
+
105
  function getDataFrame(){
106
+ document.getElementById("loadingBar").classList.remove("hidden");
107
  const wg = document.getElementById('workingGroupSelect').value;
108
  const meeting = document.getElementById('meetingSelect').value;
109
  document.getElementById('docType').innerHTML = `
 
112
  `
113
 
114
  document.getElementById('docStatus').innerHTML = `
115
+ <option disabled selected value="">Status</option>
116
  <option>Tous</option>
117
  `
118
 
119
  document.getElementById('agendaItem').innerHTML = `
120
+ <option disabled selected value="">Agenda Item</option>
121
  <option>Tous</option>
122
  `
123
  const dataFrame = document.getElementById("dataFrame");
124
+ document.getElementById("progressText").classList.remove('hidden')
125
+ document.getElementById("progressText").innerHTML = "Loading ...";
126
+ document.getElementById("loadingBar").classList.remove("hidden")
127
  fetch("/get_dataframe", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({"working_group": wg, "meeting": meeting})})
128
  .then(resp => resp.json())
129
  .then(data => {
130
  document.getElementById("filters").classList.remove("hidden")
131
+ document.getElementById("loadingBar").classList.add("hidden");
132
  const dataframeBody = dataFrame.querySelector("tbody");
133
  dataframeBody.innerHTML = "";
134
  const setType = new Set();
 
177
  })
178
  })
179
 
180
+ document.getElementById("progressText").classList.add('hidden')
181
+ document.getElementById("loadingBar").classList.add("hidden")
182
  }
183
 
184
  function filterTable() {
 
216
  })
217
  }
218
 
219
+ function generateRequirements(){
220
+ const bodyreq = tableToGenBody();
221
+ document.getElementById("progressText").classList.remove('hidden');
222
+ document.getElementById("progressText").innerHTML = "Generating requirements, please wait, it may take a while ...";
223
+ document.getElementById("loadingBar").classList.remove("hidden");
224
+
225
+ fetch("/generate_requirements", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({"documents": bodyreq})})
226
+ .then(resp => resp.json())
227
+ .then(data => {
228
+ requirements = [];
229
+ data.requirements.forEach(obj => {
230
+ obj.requirements.forEach(req => {
231
+ requirements.push({"document": obj.document, "context": obj.context, "requirement": req})
232
+ })
233
+ })
234
+
235
+ document.getElementById("loadingBar").classList.add("hidden");
236
+ document.getElementById("progressText").classList.add("hidden");
237
+ document.getElementById("reqStatus").classList.remove("hidden");
238
+ document.getElementById("getReqs").classList.add("hidden");
239
+ document.getElementById("searchReq").classList.remove("hidden");
240
+ document.getElementById("categorizeReq").classList.remove("hidden");
241
+ })
242
+ }
243
+
244
+ function queryRequirements(){
245
+ fetch("/get_reqs_from_query", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({query: document.getElementById("problemDescription").value, requirements})})
246
+ .then(resp => resp.json())
247
+ .then(data => {
248
+ const dataFrame = document.getElementById("dataFrameDiv");
249
+ const dataFrameHead = dataFrame.querySelector("thead");
250
+ const dataFrameBody = dataFrame.querySelector("tbody");
251
+
252
+ dataFrame.classList.remove("hidden");
253
+
254
+ dataFrameHead.innerHTML = `
255
+ <th>TDoc</th>
256
+ <th>Context</th>
257
+ <th>Requirement</th>
258
+ `;
259
+
260
+ dataFrameBody.innerHTML = "";
261
+
262
+ data.requirements.forEach(req => {
263
+ const tr = document.createElement("tr");
264
+ tr.innerHTML = `
265
+ <td>${req["document"]}</td>
266
+ <td>${req["context"]}</td>
267
+ <td>${req["requirement"]}</td>
268
+ `;
269
+ dataFrameBody.appendChild(tr);
270
+ })
271
+ })
272
+ }
273
+
274
+ function tableToGenBody() {
275
  // columnsMap : { "NomHeaderDansTable": "nom_voulu", ... }
276
+ let columnsMap = {"TDoc": "document", "URL": "url"};
277
+ const headers = Array.from(dataFrame.querySelectorAll('thead th')).map(th => th.innerText.trim());
 
278
 
279
  // Indices des colonnes à extraire
280
  const selectedIndices = headers
281
  .map((header, idx) => columnsMap[header] ? idx : -1)
282
  .filter(idx => idx !== -1);
283
 
284
+ return Array.from(dataFrame.querySelectorAll('tbody tr'))
285
  .filter(row => getComputedStyle(row).display !== 'none')
286
  .map(row => {
287
  const cells = Array.from(row.querySelectorAll('td'));
 
302
  document.getElementById('agendaItem').addEventListener('change', filterTable)
303
  document.getElementById("workingGroupSelect").addEventListener('change', getMeetings)
304
  document.getElementById('getTDocs').addEventListener('click', getDataFrame)
305
+ document.getElementById("getReqs").addEventListener("click", generateRequirements);
306
+ document.getElementById("queryReq").addEventListener("click", queryRequirements)
307
+ document.getElementById('searchReq').addEventListener('click', ()=>{
308
+ document.getElementById('dataFrameForm').classList.add('hidden');
309
+ document.getElementById('filters').classList.add('hidden');
310
+ document.getElementById('queryReqForm').classList.remove('hidden');
311
+ document.getElementById('dataFrameDiv').classList.add('hidden');
312
+ document.getElementById('buttons').classList.add('hidden');
313
+ })
314
  </script>
315
  </body>
316
  </html>