Lucas ARRIESSE commited on
Commit
035141c
·
1 Parent(s): 49c8d46

Prepare for splitting API routes into their own routers

Browse files
api/docs.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from fastapi.routing import APIRouter
2
+
3
+ # API router for requirement extraction from docs / doc list retrieval / download
4
+ router = APIRouter()
app.py CHANGED
@@ -30,7 +30,8 @@ from aiolimiter import AsyncLimiter
30
  load_dotenv()
31
 
32
  logging.basicConfig(
33
- level=logging.DEBUG if (os.environ.get("DEBUG_LOG", "0") == "1") else logging.INFO,
 
34
  format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s',
35
  datefmt='%Y-%m-%d %H:%M:%S'
36
  )
@@ -43,7 +44,6 @@ nltk.download('wordnet')
43
  warnings.filterwarnings("ignore")
44
 
45
  app = FastAPI(title="Requirements Extractor")
46
- app.mount("/static", StaticFiles(directory="static"), name="static")
47
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=[
48
  "*"], allow_methods=["*"], allow_origins=["*"])
49
 
@@ -54,7 +54,7 @@ llm_router = Router(model_list=[
54
  {
55
  "model": "gemini/gemini-2.0-flash",
56
  "api_key": os.environ.get("GEMINI"),
57
- "max_retries": 10,
58
  "rpm": 15,
59
  "allowed_fails": 1,
60
  "cooldown": 30,
@@ -66,17 +66,13 @@ llm_router = Router(model_list=[
66
  {
67
  "model": "gemini/gemini-2.5-flash",
68
  "api_key": os.environ.get("GEMINI"),
69
- "max_retries": 10,
70
  "rpm": 10,
71
  "allowed_fails": 1,
72
  "cooldown": 30,
73
  }
74
  }], fallbacks=[{"gemini-v2": ["gemini-v1"]}], num_retries=10, retry_after=30)
75
 
76
- limiter_mapping = {
77
- model["model_name"]: AsyncLimiter(model["litellm_params"]["rpm"], 60)
78
- for model in llm_router.model_list
79
- }
80
  lemmatizer = WordNetLemmatizer()
81
 
82
  NSMAP = {
@@ -217,10 +213,7 @@ def docx_to_txt(doc_id: str, url: str):
217
  return txt_data
218
 
219
 
220
- @app.get("/")
221
- def render_page():
222
- return FileResponse("index.html")
223
-
224
 
225
  @app.post("/get_meetings", response_model=MeetingsResponse)
226
  def get_meetings(req: MeetingsRequest):
@@ -282,7 +275,7 @@ def get_change_request_dataframe(req: DataRequest):
282
  soup = BeautifulSoup(resp.text, "html.parser")
283
  files = [item.get_text() for item in soup.select("tr td a")
284
  if item.get_text().endswith(".xlsx")]
285
-
286
  if files == []:
287
  raise HTTPException(status_code=404, detail="No XLSX has been found")
288
 
@@ -377,33 +370,19 @@ async def gen_reqs(req: RequirementsRequest, background_tasks: BackgroundTasks):
377
  return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
378
 
379
  try:
380
- model_used = "gemini-v2" # À adapter si fallback activé
381
- async with limiter_mapping[model_used]:
382
- resp_ai = await llm_router.acompletion(
383
- model=model_used,
384
- messages=[
385
- {"role": "user", "content": prompt(doc_id, full)}],
386
- response_format=RequirementsResponse
387
- )
388
  return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
 
389
  except Exception as e:
390
- if "rate limit" in str(e).lower():
391
- try:
392
- model_used = "gemini-v2" # À adapter si fallback activé
393
- async with limiter_mapping[model_used]:
394
- resp_ai = await llm_router.acompletion(
395
- model=model_used,
396
- messages=[
397
- {"role": "user", "content": prompt(doc_id, full)}],
398
- response_format=RequirementsResponse
399
- )
400
- return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
401
- except Exception as fallback_e:
402
- traceback.print_exception(fallback_e)
403
- return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
404
- else:
405
- traceback.print_exception(e)
406
- return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
407
 
408
  async def process_batch(batch):
409
  results = await asyncio.gather(*(process_document(doc) for doc in batch))
@@ -448,6 +427,9 @@ async def gen_reqs(req: RequirementsRequest, con: Request):
448
  logging.info("Generating requirements for documents: {}".format(
449
  [doc.document for doc in documents]))
450
 
 
 
 
451
  def prompt(doc_id, full):
452
  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"
453
 
@@ -463,6 +445,8 @@ async def gen_reqs(req: RequirementsRequest, con: Request):
463
  return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
464
 
465
  try:
 
 
466
  model_used = "gemini-v2"
467
  resp_ai = await llm_router.acompletion(
468
  model=model_used,
@@ -473,6 +457,8 @@ async def gen_reqs(req: RequirementsRequest, con: Request):
473
  return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
474
  except Exception as e:
475
  return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
 
 
476
 
477
  # futures for all processed documents
478
  process_futures = [_process_document(doc) for doc in documents]
@@ -525,3 +511,6 @@ def find_requirements_from_problem_description(req: ReqSearchRequest):
525
  status_code=500, detail="LLM error : Generated a wrong index, please try again.")
526
 
527
  return ReqSearchResponse(requirements=[requirements[i] for i in out_llm])
 
 
 
 
30
  load_dotenv()
31
 
32
  logging.basicConfig(
33
+ level=logging.DEBUG if (os.environ.get(
34
+ "DEBUG_LOG", "0") == "1") else logging.INFO,
35
  format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s',
36
  datefmt='%Y-%m-%d %H:%M:%S'
37
  )
 
44
  warnings.filterwarnings("ignore")
45
 
46
  app = FastAPI(title="Requirements Extractor")
 
47
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=[
48
  "*"], allow_methods=["*"], allow_origins=["*"])
49
 
 
54
  {
55
  "model": "gemini/gemini-2.0-flash",
56
  "api_key": os.environ.get("GEMINI"),
57
+ "max_retries": 5,
58
  "rpm": 15,
59
  "allowed_fails": 1,
60
  "cooldown": 30,
 
66
  {
67
  "model": "gemini/gemini-2.5-flash",
68
  "api_key": os.environ.get("GEMINI"),
69
+ "max_retries": 5,
70
  "rpm": 10,
71
  "allowed_fails": 1,
72
  "cooldown": 30,
73
  }
74
  }], fallbacks=[{"gemini-v2": ["gemini-v1"]}], num_retries=10, retry_after=30)
75
 
 
 
 
 
76
  lemmatizer = WordNetLemmatizer()
77
 
78
  NSMAP = {
 
213
  return txt_data
214
 
215
 
216
+ # ============================================= Doc routes =========================================================
 
 
 
217
 
218
  @app.post("/get_meetings", response_model=MeetingsResponse)
219
  def get_meetings(req: MeetingsRequest):
 
275
  soup = BeautifulSoup(resp.text, "html.parser")
276
  files = [item.get_text() for item in soup.select("tr td a")
277
  if item.get_text().endswith(".xlsx")]
278
+
279
  if files == []:
280
  raise HTTPException(status_code=404, detail="No XLSX has been found")
281
 
 
370
  return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
371
 
372
  try:
373
+ resp_ai = await llm_router.acompletion(
374
+ model="gemini-v2",
375
+ messages=[
376
+ {"role": "user", "content": prompt(doc_id, full)}],
377
+ response_format=RequirementsResponse
378
+ )
379
+
 
380
  return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
381
+
382
  except Exception as e:
383
+ logging.error(
384
+ f"Failed to process document {doc_id}", e, stack_info=True)
385
+ return RequirementsResponse(requirements=[DocRequirements(document=doc_id, context="Error LLM", requirements=[])]).requirements
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
  async def process_batch(batch):
388
  results = await asyncio.gather(*(process_document(doc) for doc in batch))
 
427
  logging.info("Generating requirements for documents: {}".format(
428
  [doc.document for doc in documents]))
429
 
430
+ # limit max concurrency of LLM requests to prevent a huge pile of errors because of small rate limits
431
+ concurrency_sema = asyncio.Semaphore(4)
432
+
433
  def prompt(doc_id, full):
434
  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"
435
 
 
445
  return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
446
 
447
  try:
448
+ await concurrency_sema.acquire()
449
+
450
  model_used = "gemini-v2"
451
  resp_ai = await llm_router.acompletion(
452
  model=model_used,
 
457
  return RequirementsResponse.model_validate_json(resp_ai.choices[0].message.content).requirements
458
  except Exception as e:
459
  return [DocRequirements(document=doc_id, context="Error LLM", requirements=[])]
460
+ finally:
461
+ concurrency_sema.release()
462
 
463
  # futures for all processed documents
464
  process_futures = [_process_document(doc) for doc in documents]
 
511
  status_code=500, detail="LLM error : Generated a wrong index, please try again.")
512
 
513
  return ReqSearchResponse(requirements=[requirements[i] for i in out_llm])
514
+
515
+
516
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
static/index.html ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Requirements Extractor</title>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/16.0.0/lib/marked.umd.min.js"
9
+ integrity="sha512-ygzQGrI/8Bfkm9ToUkBEuSMrapUZcHUys05feZh4ScVrKCYEXJsCBYNeVWZ0ghpH+n3Sl7OYlRZ/1ko01pYUCQ=="
10
+ crossorigin="anonymous" referrerpolicy="no-referrer"></script>
11
+ <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
12
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
13
+ </head>
14
+
15
+ <body class="bg-gray-100 min-h-screen">
16
+ <!-- Loading Overlay -->
17
+ <div id="loading-overlay" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
18
+ <div class="bg-white p-6 rounded-lg shadow-lg text-center">
19
+ <span class="loading loading-spinner loading-xl"></span>
20
+ <p id="progress-text" class="text-gray-700">Chargement en cours...</p>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="container mx-auto p-6">
25
+ <h1 class="text-3xl font-bold text-center mb-8">Requirements Extractor</h1>
26
+ <div id="selection-container" class="mb-6">
27
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
28
+ <!-- Working Group -->
29
+ <div>
30
+ <label for="working-group-select" class="block text-sm font-medium text-gray-700 mb-2">Working
31
+ Group</label>
32
+ <select id="working-group-select" class="w-full p-2 border border-gray-300 rounded-md">
33
+ <option value="" selected>Select a working group</option>
34
+ <option value="SA1">SA1</option>
35
+ <option value="SA2">SA2</option>
36
+ <option value="SA3">SA3</option>
37
+ <option value="SA4">SA4</option>
38
+ <option value="SA5">SA5</option>
39
+ <option value="SA6">SA6</option>
40
+ <option value="CT1">CT1</option>
41
+ <option value="CT2">CT2</option>
42
+ <option value="CT3">CT3</option>
43
+ <option value="CT4">CT4</option>
44
+ <option value="CT5">CT5</option>
45
+ <option value="CT6">CT6</option>
46
+ <option value="RAN1">RAN1</option>
47
+ <option value="RAN2">RAN2</option>
48
+ </select>
49
+ </div>
50
+
51
+ <!-- Meeting -->
52
+ <div>
53
+ <label for="meeting-select" class="block text-sm font-medium text-gray-700 mb-2">Meeting</label>
54
+ <select id="meeting-select" class="w-full p-2 border border-gray-300 rounded-md">
55
+ <option value="">Select a meeting</option>
56
+ </select>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Buttons -->
61
+ <div class="flex flex-col sm:flex-row gap-2">
62
+ <!-- <button id="get-meetings-btn" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
63
+ Get Meetings
64
+ </button> -->
65
+ <button id="get-tdocs-btn" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">
66
+ Get TDocs
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Add an horizontal separation -->
72
+ <hr>
73
+ <!-- Tab list for subsections -->
74
+ <div role="tablist" class="tabs tabs-border tabs-xl" id="tab-container">
75
+ <a role="tab" class="tab tab-active" id="doc-table-tab" onclick="switchTab('doc-table-tab')">📝
76
+ Documents</a>
77
+ <a role="tab" class="tab tab-disabled" id="requirements-tab" onclick="switchTab('requirements-tab')">
78
+ <div class="flex items-center gap-1">
79
+ <div class="badge badge-neutral badge-outline badge-xs" id="requirements-tab-badge">0</div>
80
+ <span>Requirements</span>
81
+ </div>
82
+ </a>
83
+ <a role="tab" class="tab tab-disabled" id="solutions-tab" onclick="switchTab('solutions-tab')">Group and
84
+ Solve</a>
85
+ <a role="tab" class="tab tab-disabled" id="query-tab" onclick="switchTab('query-tab')">🔎 Find relevant
86
+ requirements</a>
87
+ </div>
88
+
89
+ <div id="doc-table-tab-contents" class="hidden">
90
+ <!-- Filters -->
91
+ <div id="filters-container" class="mb-6 hidden pt-10">
92
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
93
+ <!-- Type Filter Dropdown -->
94
+ <div class="dropdown dropdown-bottom dropdown-center w-full mb-4">
95
+ <div tabindex="0" role="button" class="btn w-full justify-between" id="doc-type-filter-btn">
96
+ <span id="doc-type-filter-label">Type</span>
97
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24"
98
+ stroke="currentColor">
99
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
100
+ d="M19 9l-7 7-7-7" />
101
+ </svg>
102
+ </div>
103
+ <ul tabindex="0"
104
+ class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-full min-w-[200px] max-h-60 overflow-y-auto"
105
+ id="doc-type-filter-menu">
106
+ <!-- Rempli par JS -->
107
+ </ul>
108
+ </div>
109
+
110
+ <!-- Status Filter Dropdown -->
111
+ <div class="dropdown dropdown-bottom dropdown-center w-full mb-4">
112
+ <div tabindex="0" role="button" class="btn w-full justify-between" id="status-dropdown-btn">
113
+ <span id="status-filter-label">Status (Tous)</span>
114
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24"
115
+ stroke="currentColor">
116
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
117
+ d="M19 9l-7 7-7-7" />
118
+ </svg>
119
+ </div>
120
+ <ul tabindex="0"
121
+ class="dropdown-content z-[1] p-2 shadow bg-base-100 rounded-box w-full max-h-60 overflow-y-auto">
122
+ <li class="border-b pb-2 mb-2">
123
+ <label class="flex items-center gap-2 cursor-pointer">
124
+ <input type="checkbox" class="status-checkbox" value="all" checked>
125
+ <span class="font-semibold">Tous</span>
126
+ </label>
127
+ </li>
128
+ <div id="status-options" class="flex flex-col gap-1"></div>
129
+ </ul>
130
+ </div>
131
+
132
+ <!-- Agenda Item Filter Dropdown -->
133
+ <div class="dropdown dropdown-bottom dropdown-center w-full mb-4">
134
+ <div tabindex="0" role="button" class="btn w-full justify-between" id="agenda-dropdown-btn">
135
+ <span id="agenda-filter-label">Agenda Item (Tous)</span>
136
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24"
137
+ stroke="currentColor">
138
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
139
+ d="M19 9l-7 7-7-7" />
140
+ </svg>
141
+ </div>
142
+ <ul tabindex="0"
143
+ class="dropdown-content z-[1] p-2 shadow bg-base-100 rounded-box w-full max-h-60 overflow-y-auto">
144
+ <li class="border-b pb-2 mb-2">
145
+ <label class="flex items-center gap-2 cursor-pointer">
146
+ <input type="checkbox" class="agenda-checkbox" value="all" checked>
147
+ <span class="font-semibold">Tous</span>
148
+ </label>
149
+ </li>
150
+ <div id="agenda-options" class="flex flex-col gap-1"></div>
151
+ </ul>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Data Table Informations -->
157
+ <div class="flex justify-between items-center mb-2 pt-5" id="data-table-info-container">
158
+ <!-- Left side: buttons -->
159
+ <div class="flex gap-2 items-center">
160
+ <div class="tooltip" data-tip="Extract requirements from selected documents">
161
+ <button id="extract-requirements-btn"
162
+ class="bg-orange-300 text-white text-sm rounded px-3 py-1 shadow hover:bg-orange-600">
163
+ <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true"
164
+ xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"
165
+ viewBox="0 0 24 24">
166
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
167
+ stroke-width="2"
168
+ d="M9 8h6m-6 4h6m-6 4h6M6 3v18l2-2 2 2 2-2 2 2 2-2 2 2V3l-2 2-2-2-2 2-2-2-2 2-2-2Z" />
169
+ </svg>Extract Requirements
170
+ </button>
171
+ </div>
172
+ <button id="download-tdocs-btn" class="text-sm rounded px-3 py-1 shadow cursor-pointer">
173
+ 📦 Download Selected TDocs
174
+ </button>
175
+ </div>
176
+
177
+ <!-- Right side: document counts -->
178
+ <div class="flex gap-2 items-center">
179
+ <span id="displayed-count" class="text-sm text-gray-700 bg-white rounded px-3 py-1 shadow">
180
+ 0 total documents
181
+ </span>
182
+ <span id="selected-count" class="text-sm text-blue-700 bg-blue-50 rounded px-3 py-1 shadow">
183
+ 0 selected documents
184
+ </span>
185
+ </div>
186
+ </div>
187
+
188
+
189
+ <!-- Data Table -->
190
+ <div id="data-table-container" class="mb-6">
191
+ <table id="data-table" class="w-full bg-white rounded-lg shadow overflow-hidden">
192
+ <thead class="bg-gray-50">
193
+ <tr>
194
+ <th class="px-4 py-2 text-left">
195
+ <input type="checkbox" id="select-all-checkbox">
196
+ </th>
197
+ <th class="px-4 py-2 text-left">TDoc</th>
198
+ <th class="px-4 py-2 text-left">Title</th>
199
+ <th class="px-4 py-2 text-left">Type</th>
200
+ <th class="px-4 py-2 text-left">Status</th>
201
+ <th class="px-4 py-2 text-left">Agenda Item</th>
202
+ <th class="px-4 py-2 text-left">URL</th>
203
+ </tr>
204
+ </thead>
205
+ <tbody></tbody>
206
+ </table>
207
+ </div>
208
+ </div>
209
+
210
+ <div id="requirements-tab-contents" class="hidden pt-10">
211
+ <!-- Requirement list container -->
212
+ <div id="requirements-container" class="mb-6">
213
+ <div class="flex">
214
+ <h2 class="text-2xl font-bold mb-4">Extracted requirement list</h2>
215
+ <div class="justify-end pl-5">
216
+ <!--Copy ALL reqs button-->
217
+ <div class="tooltip" data-tip="Copy ALL requirements to clipboard">
218
+ <button class="btn btn-square" id="copy-all-reqs-btn" aria-label="Copy">
219
+ 📋
220
+ </button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ <div id="requirements-list"></div>
225
+ </div>
226
+ </div>
227
+
228
+
229
+ <!-- Query Requirements -->
230
+ <div id="query-tab-contents" class="hidden pt-10">
231
+ <div id="query-requirements-container" class="mb-6">
232
+ <h2 class="text-2xl font-bold mb-4">Find relevant requirements for a query</h2>
233
+ <div class="flex gap-2">
234
+ <input type="text" id="query-input" class="flex-1 p-2 border border-gray-300 rounded-md"
235
+ placeholder="Enter your query...">
236
+ <button id="search-requirements-btn"
237
+ class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">
238
+ Search
239
+ </button>
240
+ </div>
241
+ <div id="query-results" class="mt-4"></div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Solution tab -->
246
+ <div id="solutions-tab-contents" class="hidden pt-10">
247
+ <!--Header de catégorisation des requirements-->
248
+ <div class="w-full mx-auto mt-4 p-4 bg-base-100 rounded-box shadow-lg border border-base-200">
249
+ <!-- Séléction du nb de catégories / mode auto-->
250
+ <div class="flex flex-wrap justify-begin items-center gap-6">
251
+ <!-- Number input -->
252
+ <div class="form-control flex flex-col items-center justify-center">
253
+ <label class="label" for="category-count">
254
+ <span class="label-text text-base-content"># Categories</span>
255
+ </label>
256
+ <input id="category-count" type="number" class="input input-bordered w-24" min="1" value="3" />
257
+ </div>
258
+
259
+ <!-- Auto detect toggle -->
260
+ <div class="form-control flex flex-col items-center justify-center">
261
+ <label class="label">
262
+ <span class="label-text text-base-content">Number of categories</span>
263
+ </label>
264
+ <input type="checkbox" class="toggle toggle-primary" id="auto-detect-toggle" checked="true" />
265
+ <div class="text-xs mt-1 text-base-content">Manual | Auto detect</div>
266
+ </div>
267
+
268
+ <!-- Action Button -->
269
+ <button id="categorize-requirements-btn" class="btn btn-primary btn-l self-center">
270
+ Categorize
271
+ </button>
272
+ </div>
273
+ <div class="flex flex-wrap justify-end items-center gap-6">
274
+ <div class="tooltip" data-tip="Use Insight Finder's API to generate solutions">
275
+ <label class="label">
276
+ <span class="label-text text-base-content">Use insight finder solver</span>
277
+ </label>
278
+ <input type="checkbox" class="toggle toggle-primary" id="use-insight-finder-solver"
279
+ checked="false" />
280
+ </div>
281
+ </div>
282
+ </div>
283
+ <div id="categorized-requirements-container pt-10" class="mb-6">
284
+ <div class="flex mb-4 pt-10">
285
+ <div class="justify-begin">
286
+ <h2 class="text-2xl font-bold">Requirements categories list</h2>
287
+ </div>
288
+ <div class="justify-end pl-5">
289
+ <!--Copy reqs button-->
290
+ <div class="tooltip" data-tip="Copy selected requirements to clipboard">
291
+ <button class="btn btn-square" id="copy-reqs-btn" aria-label="Copy">
292
+ 📋
293
+ </button>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ <div id="categorized-requirements-list"></div>
298
+ </div>
299
+
300
+ <!-- Solutions Action Buttons -->
301
+ <div id="solutions-action-buttons-container" class="mb-6 hidden">
302
+ <div class="flex flex-wrap gap-2 justify-center items-center">
303
+ <div class="join">
304
+ <input id="solution-gen-nsteps" type="number" class="input join-item w-24" min="1" value="3" />
305
+ <button id="get-solutions-btn"
306
+ class="btn join-item px-4 py-2 bg-indigo-500 text-white rounded-md hover:bg-indigo-600">
307
+ Run multiple steps
308
+ </button>
309
+ </div>
310
+ <button id="get-solutions-step-btn"
311
+ class="px-4 py-2 bg-pink-500 text-white rounded-md hover:bg-pink-600">
312
+ Get Solutions (Step-by-step)
313
+ </button>
314
+ <div class="dropdown dropdown-center">
315
+ <div id="additional-gen-instr-btn" tabindex="0" role="button" class="btn m-1">🎨 Additional
316
+ generation constraints</div>
317
+ <div tabindex="0" class="dropdown-content card card-sm bg-base-100 z-1 w-256 shadow-md">
318
+ <div class="card-body">
319
+ <h3>Add additional constraints to follow when generating the solutions</h3>
320
+ <textarea id="additional-gen-instr"
321
+ class="textarea textarea-bordered w-full"></textarea>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ </div>
328
+
329
+ <!-- Solutions Container -->
330
+ <div id="solutions-container" class="mb-6">
331
+ <h2 class="text-2xl font-bold mb-4">Solutions</h2>
332
+ <div id="solutions-list"></div>
333
+ </div>
334
+ </div>
335
+
336
+ <script src="js/sse.js"></script>
337
+ <script src="js/ui-utils.js"></script>
338
+ <script src="js/script.js"></script>
339
+ </body>
340
+
341
+ </html>
static/{script.js → js/script.js} RENAMED
File without changes
static/{sse.js → js/sse.js} RENAMED
File without changes
static/{ui-utils.js → js/ui-utils.js} RENAMED
File without changes