Lucas ARRIESSE commited on
Commit
e97be0e
·
1 Parent(s): 75ac1c4

WIP solution drafting

Browse files
README.md CHANGED
@@ -10,3 +10,11 @@ short_description: Requirements Extractor
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ ## Used libraries
15
+
16
+ **Check requirements.txt for server-side libraries**
17
+
18
+ **Client side libraries**
19
+ - `markedjs` : For rendering markdown content.
20
+ - `zod` : For clientside schema validation.
api/requirements.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from fastapi import APIRouter, Depends, HTTPException
2
  from jinja2 import Environment
3
  from litellm.router import Router
@@ -22,12 +23,12 @@ def find_requirements_from_problem_description(req: ReqSearchRequest, llm_router
22
  messages=[{"role": "user", "content": f"Given all the requirements : \n {requirements_text} \n and the problem description \"{query}\", return a list of 'Selection ID' for the most relevant corresponding requirements that reference or best cover the problem. If none of the requirements covers the problem, simply return an empty list"}],
23
  response_format=ReqSearchLLMResponse
24
  )
25
- print("Answered")
26
- print(resp_ai.choices[0].message.content)
27
 
28
  out_llm = ReqSearchLLMResponse.model_validate_json(
29
  resp_ai.choices[0].message.content).selected
30
 
 
 
31
  if max(out_llm) > len(requirements) - 1:
32
  raise HTTPException(
33
  status_code=500, detail="LLM error : Generated a wrong index, please try again.")
 
1
+ import logging
2
  from fastapi import APIRouter, Depends, HTTPException
3
  from jinja2 import Environment
4
  from litellm.router import Router
 
23
  messages=[{"role": "user", "content": f"Given all the requirements : \n {requirements_text} \n and the problem description \"{query}\", return a list of 'Selection ID' for the most relevant corresponding requirements that reference or best cover the problem. If none of the requirements covers the problem, simply return an empty list"}],
24
  response_format=ReqSearchLLMResponse
25
  )
 
 
26
 
27
  out_llm = ReqSearchLLMResponse.model_validate_json(
28
  resp_ai.choices[0].message.content).selected
29
 
30
+ logging.info(f"Found {len(out_llm)} reqs matching case.")
31
+
32
  if max(out_llm) > len(requirements) - 1:
33
  raise HTTPException(
34
  status_code=500, detail="LLM error : Generated a wrong index, please try again.")
api/solutions.py CHANGED
@@ -1,13 +1,13 @@
1
  import asyncio
2
  import json
3
  import logging
4
- from fastapi import APIRouter, Depends, HTTPException
5
  from httpx import AsyncClient
6
- from jinja2 import Environment
7
  from litellm.router import Router
8
  from dependencies import INSIGHT_FINDER_BASE_URL, get_http_client, get_llm_router, get_prompt_templates
9
  from typing import Awaitable, Callable, TypeVar
10
- from schemas import _RefinedSolutionModel, _BootstrappedSolutionModel, _SolutionCriticismOutput, CriticizeSolutionsRequest, CritiqueResponse, InsightFinderConstraintsList, ReqGroupingCategory, ReqGroupingRequest, ReqGroupingResponse, ReqSearchLLMResponse, ReqSearchRequest, ReqSearchResponse, SolutionCriticism, SolutionModel, SolutionBootstrapResponse, SolutionBootstrapRequest, TechnologyData
11
 
12
  # Router for solution generation and critique
13
  router = APIRouter(tags=["solution generation and critique"])
@@ -67,7 +67,7 @@ async def bootstrap_solutions(req: SolutionBootstrapRequest, prompt_env: Environ
67
 
68
  format_solution = await llm_router.acompletion("gemini-v2", messages=[{
69
  "role": "user",
70
- "content": await prompt_env.get_template("synthesize_solution.txt").render_async(**{
71
  "category": cat.model_dump(),
72
  "technologies": technologies.model_dump()["technologies"],
73
  "user_constraints": req.user_constraints,
@@ -155,3 +155,40 @@ async def refine_solutions(params: CritiqueResponse, prompt_env: Environment = D
155
  refined_solutions = await asyncio.gather(*[__refine_solution(crit) for crit in params.critiques], return_exceptions=False)
156
 
157
  return SolutionBootstrapResponse(solutions=refined_solutions)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import asyncio
2
  import json
3
  import logging
4
+ from fastapi import APIRouter, Depends, HTTPException, Response
5
  from httpx import AsyncClient
6
+ from jinja2 import Environment, TemplateNotFound
7
  from litellm.router import Router
8
  from dependencies import INSIGHT_FINDER_BASE_URL, get_http_client, get_llm_router, get_prompt_templates
9
  from typing import Awaitable, Callable, TypeVar
10
+ from schemas import _RefinedSolutionModel, _BootstrappedSolutionModel, _SolutionCriticismOutput, CriticizeSolutionsRequest, CritiqueResponse, InsightFinderConstraintsList, PriorArtSearchRequest, PriorArtSearchResponse, ReqGroupingCategory, ReqGroupingRequest, ReqGroupingResponse, ReqSearchLLMResponse, ReqSearchRequest, ReqSearchResponse, SolutionCriticism, SolutionModel, SolutionBootstrapResponse, SolutionBootstrapRequest, TechnologyData
11
 
12
  # Router for solution generation and critique
13
  router = APIRouter(tags=["solution generation and critique"])
 
67
 
68
  format_solution = await llm_router.acompletion("gemini-v2", messages=[{
69
  "role": "user",
70
+ "content": await prompt_env.get_template("bootstrap_solution.txt").render_async(**{
71
  "category": cat.model_dump(),
72
  "technologies": technologies.model_dump()["technologies"],
73
  "user_constraints": req.user_constraints,
 
155
  refined_solutions = await asyncio.gather(*[__refine_solution(crit) for crit in params.critiques], return_exceptions=False)
156
 
157
  return SolutionBootstrapResponse(solutions=refined_solutions)
158
+
159
+
160
+ @router.post("/search_prior_art")
161
+ async def search_prior_art(req: PriorArtSearchRequest, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router)) -> PriorArtSearchResponse:
162
+ """Performs a comprehensive prior art search / FTO search against the provided topics for a drafted solution"""
163
+
164
+ sema = asyncio.Semaphore(4)
165
+
166
+ async def __search_topic(topic: str) -> str:
167
+ search_prompt = await prompt_env.get_template("search/search_topic.txt").render_async(**{
168
+ "topic": topic
169
+ })
170
+
171
+ try:
172
+ await sema.acquire()
173
+
174
+ search_completion = await llm_router.acompletion(model="gemini-v2", messages=[
175
+ {"role": "user", "content": search_prompt}
176
+ ], temperature=0.3, tools=[{"googleSearch": {}}])
177
+
178
+ return {"topic": topic, "content": search_completion.choices[0].message.content}
179
+ finally:
180
+ sema.release()
181
+
182
+ # Dispatch the individual tasks for topic search
183
+ topics = await asyncio.gather(*[__search_topic(top) for top in req.topics], return_exceptions=False)
184
+
185
+ consolidation_prompt = await prompt_env.get_template("search/build_final_report.txt").render_async(**{
186
+ "searches": topics
187
+ })
188
+
189
+ # Then consolidate everything into a single detailed topic
190
+ consolidation_completion = await llm_router.acompletion(model="gemini-v2", messages=[
191
+ {"role": "user", "content": consolidation_prompt}
192
+ ], temperature=0.5)
193
+
194
+ return PriorArtSearchResponse(content=consolidation_completion.choices[0].message.content, references=[])
app.py CHANGED
@@ -2,13 +2,14 @@ import asyncio
2
  import logging
3
  from dotenv import load_dotenv
4
  from typing import Literal
 
5
  import nltk
6
  import warnings
7
  import os
8
- from fastapi import Depends, FastAPI, BackgroundTasks, HTTPException, Request
9
  from fastapi.staticfiles import StaticFiles
10
  import api.solutions
11
- from dependencies import get_llm_router, init_dependencies
12
  import api.docs
13
  import api.requirements
14
  from api.docs import docx_to_txt
@@ -40,9 +41,22 @@ app = FastAPI(title="Requirements Extractor", docs_url="/apidocs")
40
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=[
41
  "*"], allow_methods=["*"], allow_origins=["*"])
42
 
43
- # =======================================================================================================================================================================================
44
 
45
  app.include_router(api.docs.router, prefix="/docs")
46
  app.include_router(api.requirements.router, prefix="/requirements")
47
  app.include_router(api.solutions.router, prefix="/solutions")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  app.mount("/", StaticFiles(directory="static", html=True), name="static")
 
2
  import logging
3
  from dotenv import load_dotenv
4
  from typing import Literal
5
+ from jinja2 import Environment, TemplateNotFound
6
  import nltk
7
  import warnings
8
  import os
9
+ from fastapi import Depends, FastAPI, BackgroundTasks, HTTPException, Request, Response
10
  from fastapi.staticfiles import StaticFiles
11
  import api.solutions
12
+ from dependencies import get_llm_router, get_prompt_templates, init_dependencies
13
  import api.docs
14
  import api.requirements
15
  from api.docs import docx_to_txt
 
41
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=[
42
  "*"], allow_methods=["*"], allow_origins=["*"])
43
 
 
44
 
45
  app.include_router(api.docs.router, prefix="/docs")
46
  app.include_router(api.requirements.router, prefix="/requirements")
47
  app.include_router(api.solutions.router, prefix="/solutions")
48
+
49
+ # INTERNAL ROUTE TO RETRIEVE PROMPT TEMPLATES FOR PRIVATE COMPUTE
50
+ @app.get("/prompt/{task}", include_in_schema=True)
51
+ async def retrieve_prompt(task: str, prompt_env: Environment = Depends(get_prompt_templates)):
52
+ """Retrieves a prompt for client-side private inference"""
53
+ try:
54
+ logging.info(f"Retrieving template for on device private task {task}.")
55
+ prompt, filename, _ = prompt_env.loader.get_source(
56
+ prompt_env, f"private/{task}.txt")
57
+ return prompt
58
+ except TemplateNotFound as _:
59
+ return Response(content="", status_code=404)
60
+
61
+
62
  app.mount("/", StaticFiles(directory="static", html=True), name="static")
prompts/{synthesize_solution.txt → bootstrap_solution.txt} RENAMED
@@ -26,7 +26,6 @@ Requirements:
26
  Here are the technologies you may use for the mechanisms that compose the solution:
27
  {% for tech in technologies -%}
28
  - {{tech["title"]}} : {{tech["purpose"]}}
29
- * Key Components : {{tech["key_components"]}}
30
  {% endfor %}
31
  </available_technologies>
32
 
 
26
  Here are the technologies you may use for the mechanisms that compose the solution:
27
  {% for tech in technologies -%}
28
  - {{tech["title"]}} : {{tech["purpose"]}}
 
29
  {% endfor %}
30
  </available_technologies>
31
 
prompts/private/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Prompts for client-side inference (in `private/`) should only use basic variable templating.
prompts/private/assess.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <role> You are a patent committee expert, critical and no sugarcoating. Your criteria of selection of an idea is whether the idea would more likely lead to a patentable solution that add value and can be monetized to/by your business.</role>
2
+ <task>
3
+ Analyze the patentability and added value of the proposed invention. Ensure that it ensures with your business line and patent portfolio.
4
+ Patentability criteria are originality, innovative process, detectable,non-obvious by a person of the art, surprising.
5
+ Evaluate the patent using the following notation criterias while taking into account the business line and portfolio.
6
+ Finally end your analysis by stating whether the idea is a "NO-GO", "CONDITIONAL-GO", "IMMEDIATE-GO" and provide a list of actionnable insights to help refine substantially the idea to better align to the business line if need be.
7
+ </task>
8
+
9
+ <business>
10
+ {{business}}
11
+ </business>
12
+
13
+ <notation_criterias>
14
+ {{notation_criterias}}
15
+ </notation_criterias>
16
+
17
+ <idea>
18
+ **The idea is as follows:**
19
+
20
+ ## Problem description
21
+ {{problem_description}}
22
+
23
+ ## Solution description
24
+ {{solution_description}}
25
+ </idea>
prompts/private/extract.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <role>You are an useful assistant great at summarizing and extracting insight from reports</role>
2
+ <task>Extract from the report you're given the final verdict for the evaluated idea ("NO-GO", "CONDITIONAL-GO", "IMMEDIATE-GO"), summarize the global report feedback
3
+ and extract the actionnable insights.
4
+ </task>
5
+
6
+ <report>
7
+ {{report}}
8
+ </report>
9
+
10
+ <response_format>
11
+ Reply in JSON using the following schema:
12
+ {{response_schema}}
13
+
14
+
15
+ Here's an example response:
16
+ {
17
+ "final_verdict": "NO-GO",
18
+ "summary": "<summary>",
19
+ "insights": [
20
+ "Develop the training part",
21
+ "Review the model architechture design"
22
+ ]
23
+ }
24
+ </response_format>
prompts/private/refine.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <role>You are an expert system designer</role>
2
+ <task>
3
+ Your task is to refine an idea to account for the given insights.
4
+ </task>
5
+
6
+ <idea>
7
+ **The idea is as follows:**
8
+
9
+ ## Problem description
10
+ {{problem_description}}
11
+
12
+ ## Solution description
13
+ {{solution_description}}
14
+ </idea>
15
+
16
+ <insights>
17
+ Here are the insights:
18
+ {{insights}}
19
+ </insights>
20
+
21
+ <business>
22
+ Here is some business info to help for refining based on the insights:
23
+ {{business_info}}
24
+ </business>
prompts/search/build_final_report.txt ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <role>You are a deep researcher agent which excels in prior art analysis for ideas</role>
2
+ <task>
3
+ Your task is to consolidate a list of searches done for multiple topics in a single report detailling the current prior art for a solution.
4
+ - Present a high-level summary of the most relevant prior art identified, mentioning the general approaches and technologies.
5
+
6
+ For each identified piece of prior art (patent or academic document):
7
+
8
+ * **Identification:**
9
+ * **Type:** Patent / Academic Paper
10
+ * **Identifier:** [Patent Number/Publication Number or Paper Title/DOI]
11
+ * **Title:** [Title of Patent/Paper]
12
+ * **Authors/Inventors:** [Names]
13
+ * **Publication Date:** [Date]
14
+ * **Assignee/Publisher:** [Company/Institution/Journal]
15
+ * **Link/Source:** [URL or DOI if applicable]
16
+
17
+ * **Mechanism Description:**
18
+ * Detail the core working principle and mechanism(s) described in the prior art.
19
+ * Explain how it functions and the underlying scientific or engineering concepts.
20
+ * [Specific instructions on detail level - e.g., "Describe at a conceptual level", "Include schematic representations if described", "Focus on the functional blocks"]
21
+
22
+ * **Key Features:**
23
+ * List and describe the significant features of the mechanism.
24
+ * Highlight unique aspects, advantages, and potential applications.
25
+ * [Specific instructions on features to focus on - e.g., "Focus on efficiency metrics", "Describe material choices", "Mention any novel components"]
26
+
27
+ * **Advantages and Disadvantages/Limitations:**
28
+ * Outline the benefits and drawbacks of the described mechanism.
29
+ * Identify any limitations or potential challenges.
30
+
31
+ * **Relevance to topic:**
32
+ * Clearly explain how this prior art relates to the given topic.
33
+ * Quantify or qualify the degree of relevance if possible.
34
+
35
+ - Comparison of Prior Art
36
+
37
+ * If multiple similar prior arts are found, create a comparative analysis.
38
+ * Highlight similarities and differences in mechanisms, features, and performance.
39
+ * Consider presenting this in a table format.
40
+
41
+ - Gaps and Opportunities
42
+
43
+ * Based on the analysis, identify areas where current prior art is lacking or could be improved.
44
+ * Suggest potential opportunities for innovation or further development in relation to topic.
45
+
46
+ - Conclusion
47
+
48
+ * Summarize the key findings of the prior art search.
49
+ * Provide a concluding statement on the landscape of existing technologies related to topic.
50
+ </task>
51
+
52
+ <searches>
53
+ {% for search in searches -%}
54
+ ### Topic: {{search['topic']}}
55
+ {{search['content']}}
56
+ {% endfor %}
57
+ </searches>
prompts/search/search_topic.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <role>You are a deep researcher agent which excels in prior art analysis for ideas</role>
2
+ <task>
3
+ Your task is to search prior art which will also serve for Freedom-to-Operate analysis on a following given topic.
4
+ Analyze patents and academic documents and detail the found mechanisms and their features.
5
+ </task>
6
+
7
+ <topic>
8
+ **The topic is**
9
+ {{topic}}
10
+
11
+ Use keywords adapted and relevant to the context to perform your searches.
12
+ </topic>
schemas.py CHANGED
@@ -1,5 +1,5 @@
1
  from pydantic import BaseModel, Field
2
- from typing import Any, List, Dict, Optional
3
 
4
 
5
  class MeetingsRequest(BaseModel):
@@ -90,7 +90,7 @@ class SolutionModel(BaseModel):
90
  description="Detailed description of the solution.")
91
  references: list[dict] = Field(
92
  ..., description="References to documents used for the solution.")
93
-
94
  category_id: int = Field(
95
  ..., description="ID of the requirements category the solution is based on")
96
 
@@ -126,6 +126,30 @@ class _ReqGroupingOutput(BaseModel):
126
 
127
 
128
  # =================================================================== bootstrap solution response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  class _SolutionBootstrapOutput(BaseModel):
131
  solution: SolutionModel
@@ -183,39 +207,22 @@ class CritiqueResponse(BaseModel):
183
  # ==========================================================================
184
 
185
  class _RefinedSolutionModel(BaseModel):
186
- """Internal model used for solution refining"""
187
 
188
  problem_description: str = Field(...,
189
  description="New description of the problem being solved.")
190
  solution_description: str = Field(...,
191
  description="New detailed description of the solution.")
192
 
 
193
 
194
- # ================================================================= search solutions using insight finder endpoints
 
 
 
195
 
196
- # Helpers to extract constraints for Insights Finder
197
 
198
- class InsightFinderConstraintItem(BaseModel):
199
- title: str
200
- description: str
201
-
202
-
203
- class InsightFinderConstraintsList(BaseModel):
204
- constraints: list[InsightFinderConstraintItem]
205
-
206
- # =================================================
207
-
208
-
209
- class Technology(BaseModel):
210
- """Represents a single technology entry with its details."""
211
- title: str
212
- purpose: str
213
- key_components: str
214
- advantages: str
215
- limitations: str
216
- id: int
217
-
218
-
219
- class TechnologyData(BaseModel):
220
- """Represents the top-level object containing a list of technologies."""
221
- technologies: List[Technology]
 
1
  from pydantic import BaseModel, Field
2
+ from typing import Any, List, Dict, Literal, Optional
3
 
4
 
5
  class MeetingsRequest(BaseModel):
 
90
  description="Detailed description of the solution.")
91
  references: list[dict] = Field(
92
  ..., description="References to documents used for the solution.")
93
+
94
  category_id: int = Field(
95
  ..., description="ID of the requirements category the solution is based on")
96
 
 
126
 
127
 
128
  # =================================================================== bootstrap solution response
129
+ # Helpers for integration with insights finder.
130
+
131
+ class InsightFinderConstraintItem(BaseModel):
132
+ title: str
133
+ description: str
134
+
135
+
136
+ class InsightFinderConstraintsList(BaseModel):
137
+ constraints: list[InsightFinderConstraintItem]
138
+
139
+ #TODO: aller voir la doc, breakage API
140
+ class Technology(BaseModel):
141
+ """Represents a single technology entry with its details."""
142
+ title: str = Field(..., alias="name")
143
+ purpose: str
144
+ advantages: str
145
+ limitations: str
146
+
147
+
148
+
149
+ class TechnologyData(BaseModel):
150
+ """Represents the top-level object containing a list of technologies."""
151
+ technologies: List[Technology]
152
+
153
 
154
  class _SolutionBootstrapOutput(BaseModel):
155
  solution: SolutionModel
 
207
  # ==========================================================================
208
 
209
  class _RefinedSolutionModel(BaseModel):
210
+ """Internal model used for bootstrapped solution refining"""
211
 
212
  problem_description: str = Field(...,
213
  description="New description of the problem being solved.")
214
  solution_description: str = Field(...,
215
  description="New detailed description of the solution.")
216
 
217
+ # ===========================================================================
218
 
219
+ class PriorArtSearchRequest(BaseModel):
220
+ topics: list[str] = Field(
221
+ ..., description="The list of topics to search for to create an exhaustive prior art search for a problem draft.")
222
+ mode: Literal['prior_art', 'fto'] = Field(default="fto", description="")
223
 
 
224
 
225
+ class PriorArtSearchResponse(BaseModel):
226
+ content: str
227
+ references: list[dict]
228
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/index.html CHANGED
@@ -5,9 +5,7 @@
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>
@@ -75,16 +73,16 @@
75
  <div role="tablist" class="tabs tabs-border tabs-xl" id="tab-container">
76
  <a role="tab" class="tab tab-active" id="doc-table-tab">📝
77
  Documents</a>
78
- <a role="tab" class="tab tab-disabled" id="requirements-tab">
79
  <div class="flex items-center gap-1">
80
  <div class="badge badge-neutral badge-outline badge-xs" id="requirements-tab-badge">0</div>
81
  <span>Requirements</span>
82
  </div>
83
  </a>
84
- <a role="tab" class="tab tab-disabled" id="solutions-tab">Group and
85
- Solve</a>
86
- <a role="tab" class="tab tab-disabled" id="query-tab">🔎 Find relevant
87
- requirements</a>
88
  </div>
89
 
90
  <div id="doc-table-tab-contents" class="hidden">
@@ -323,20 +321,100 @@
323
  </div>
324
  </div>
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
  <!--App settings modal container-->
328
  <dialog id="settings_modal" class="modal">
329
  <div class="modal-box w-11/12 max-w-5xl">
330
- <h3 class="text-lg font-bold">Reqxtract settings</h3>
331
- <p class="py-4">Enter your LLM provider URL and key to enable using private solution assessment and
332
- refining</p>
333
  <div class="modal-action">
334
  <form method="dialog">
335
  <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
336
  </form>
337
  </div>
338
  <h2 class="text-lg font-bold">Private generation settings</h2>
339
- <form class="space-y-4">
 
340
  <div>
341
  <label for="provider-url" class="block mb-2 text-sm font-medium">LLM provider URL</label>
342
  <input id="settings-provider-url" name="provider-url" class="input input-bordered w-full">
@@ -348,6 +426,13 @@
348
  type="password">
349
  </div>
350
 
 
 
 
 
 
 
 
351
  <div>
352
  <label for="assessment-rules" class="block mb-2 text-sm font-medium">Assessment rules</label>
353
  <textarea id="settings-assessment-rules" name="assessment-rules"
@@ -361,15 +446,13 @@
361
  class="textarea textarea-bordered w-full h-48"
362
  placeholder="Enter your portfolio info here..."></textarea>
363
  </div>
364
- </form>
365
  <div class="flex">
366
  <button class="btn btn-success" id="test-btn">Save config</button>
367
  </div>
368
  </div>
369
  </dialog>
370
 
371
- <script type="module" src="js/sse.js"></script>
372
- <script type="module" src="js/ui-utils.js"></script>
373
  <script type="module" src="js/app.js"></script>
374
  </body>
375
 
 
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>Requirements Extractor</title>
8
+ <!--See JS imports for ESM modules-->
 
 
9
  <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
10
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
11
  </head>
 
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">📝
75
  Documents</a>
76
+ <a role="tab" class="tab tab-disabled" id="requirements-tab" title="List all requirements">
77
  <div class="flex items-center gap-1">
78
  <div class="badge badge-neutral badge-outline badge-xs" id="requirements-tab-badge">0</div>
79
  <span>Requirements</span>
80
  </div>
81
  </a>
82
+ <a role="tab" class="tab tab-disabled" id="solutions-tab"
83
+ title="Group requirements into categories and bootstrap a solution">🔨 Group and Bootstrap</a>
84
+ <a role="tab" class="tab tab-disabled hidden" id="draft-tab">🖋 Draft and Assess</a>
85
+ <a role="tab" class="tab tab-disabled" id="query-tab">🔎 Find relevant requirements</a>
86
  </div>
87
 
88
  <div id="doc-table-tab-contents" class="hidden">
 
321
  </div>
322
  </div>
323
 
324
+ <div id="draft-tab-contents" class="mb-6 hidden">
325
+ <h2 class="text-2xl font-bold mt-4">Draft and assess solution</h2>
326
+ <div id="draft-container" class="mt-4">
327
+ <div class="max-w-9xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-8">
328
+ <!-- Actual solution draft container -->
329
+ <div class="lg:col-span-2 flex flex-col gap-6">
330
+ <div id="draft-assessment-container" class="flex-col gap-6">
331
+ <!-- card with the solution -->
332
+ <div class="card bg-base-100 shadow-xl">
333
+ <div class="card-body">
334
+ <h2 class="card-title text-2xl mb-2 font-bold">Solution Draft</h2>
335
+ <div id="solution-draft-display" class="space-y-2">
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <!-- Assessment and Insights Card -->
341
+ <div class="card bg-base-100 shadow-xl mt-4">
342
+ <div class="card-body">
343
+ <h2 class="text-xl font-bold">Assessment Result</h2>
344
+ <div id="assessment-results" class="space-y-2">
345
+ <div>
346
+ <p class="font-bold text-m">Patcom recommendation: </p>
347
+ <p class="text-m" id="assessment-recommendation-status">NO-GO</p>
348
+ </div>
349
+ <div>
350
+ <p class="font-bold text-m">Quick summary:</p>
351
+ <p id="assessment-recommendation-summary">A short resumé of the patcom sayings should go there.</p>
352
+ </div>
353
+ <button class="btn btn-info" id="read-assessment-button">Read whole assessment</button>
354
+ </div>
355
+
356
+ <div class="divider my-1"></div>
357
+
358
+ <h3 class="text-lg font-bold mt-2">Actionable Insights</h3>
359
+ <p class="text-sm text-base-content/70">Select insights below to guide the next
360
+ refinement.
361
+ </p>
362
+ <div id="insights-container" class="form-control mt-4 space-y-2">
363
+ <!-- Checkboxes will be dynamically inserted here -->
364
+ </div>
365
+ <div class="card-actions justify-end mt-6">
366
+ <button id="refine-btn" class="btn btn-primary">Refine with Selected
367
+ Insights</button>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ </div>
374
+
375
+ <!-- draft timeline -->
376
+ <div class="lg:col-span-1">
377
+ <div class="card bg-base-100 shadow-xl">
378
+ <div class="card-body">
379
+ <h2 class="card-title">Draft timeline</h2>
380
+ <div class="flex justify-center p-4 mt-4">
381
+ <ul id="timeline-container" class="steps steps-vertical">
382
+ <li class="step">Enter initial solution draft</li>
383
+ </ul>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ <!--full screen modal to read the full assessment-->
392
+ <dialog id="read-assessment-modal" class="modal">
393
+ <div class="modal-box w-11/12 max-w-5xl">
394
+ <div class="modal-action">
395
+ <form method="dialog">
396
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
397
+ </form>
398
+ </div>
399
+ <div id="read-assessment-content">
400
+ <p>Nothing to read here</p>
401
+ </div>
402
+ </dialog>
403
+ </div>
404
+
405
 
406
  <!--App settings modal container-->
407
  <dialog id="settings_modal" class="modal">
408
  <div class="modal-box w-11/12 max-w-5xl">
409
+ <h3 class="text-lg font-bold">Application settings</h3>
 
 
410
  <div class="modal-action">
411
  <form method="dialog">
412
  <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
413
  </form>
414
  </div>
415
  <h2 class="text-lg font-bold">Private generation settings</h2>
416
+ <p class="py-4">Detail your private LLM credentials under to draft and generate solution using private LLM</p>
417
+ <div class="space-y-4">
418
  <div>
419
  <label for="provider-url" class="block mb-2 text-sm font-medium">LLM provider URL</label>
420
  <input id="settings-provider-url" name="provider-url" class="input input-bordered w-full">
 
426
  type="password">
427
  </div>
428
 
429
+ <div>
430
+ <label for="provider-model" class="block mb-2 text-sm font-medium">Model</label>
431
+ <button id="settings-fetch-models" class="btn btn-outline">Fetch models</button>
432
+ <select id="settings-provider-model" name="provider-model" class="select">
433
+ </select>
434
+ </div>
435
+
436
  <div>
437
  <label for="assessment-rules" class="block mb-2 text-sm font-medium">Assessment rules</label>
438
  <textarea id="settings-assessment-rules" name="assessment-rules"
 
446
  class="textarea textarea-bordered w-full h-48"
447
  placeholder="Enter your portfolio info here..."></textarea>
448
  </div>
449
+ </div>
450
  <div class="flex">
451
  <button class="btn btn-success" id="test-btn">Save config</button>
452
  </div>
453
  </div>
454
  </dialog>
455
 
 
 
456
  <script type="module" src="js/app.js"></script>
457
  </body>
458
 
static/js/app.js CHANGED
@@ -1,9 +1,9 @@
1
 
2
  import {
3
  toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect,
4
- populateCheckboxDropdown, updateCheckboxDropdownLabel, updateSelectedFilters, populateDaisyDropdown, updateFilterLabel,
5
- extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount,
6
- bindTabs, checkCanUsePrivateGen
7
  } from "./ui-utils.js";
8
  import { postWithSSE } from "./sse.js";
9
 
@@ -15,15 +15,18 @@ let selectedType = ""; // "" = Tous
15
  let selectedStatus = new Set(); // valeurs cochées (hors "Tous")
16
  let selectedAgenda = new Set();
17
 
18
- // Generation de solutions
19
- let accordionStates = {};
20
  let formattedRequirements = [];
21
  let categorizedRequirements = [];
 
 
 
 
 
22
  let solutionsCriticizedVersions = [];
23
  // checksum pour vérifier si les requirements séléctionnés ont changé
24
  let lastSelectedRequirementsChecksum = null;
25
- // les requirements ont ils été extraits au moins une fois ?
26
- let hasRequirementsExtracted = false;
27
 
28
  // =============================================================================
29
  // FONCTIONS MÉTIER
@@ -428,7 +431,7 @@ async function categorizeRequirements(max_categories) {
428
  const data = await response.json();
429
  categorizedRequirements = data;
430
  displayCategorizedRequirements(categorizedRequirements.categories);
431
- clearAllSolutions();
432
 
433
  // Masquer le container de query et afficher les catégories et boutons solutions
434
  // toggleContainersVisibility(['query-requirements-container'], false);
@@ -586,7 +589,6 @@ function copySelectedRequirementsAsMarkdown() {
586
  const markdownText = lines.join('\n');
587
 
588
  navigator.clipboard.writeText(markdownText).then(() => {
589
- console.log("Markdown copied to clipboard.");
590
  alert("Selected requirements copied to clipboard");
591
  }).catch(err => {
592
  console.error("Failed to copy markdown:", err);
@@ -718,6 +720,8 @@ function displaySearchResults(results) {
718
  container.appendChild(resultsDiv);
719
  }
720
 
 
 
721
  function createSolutionAccordion(solutionCriticizedHistory, containerId, versionIndex = 0, categoryIndex = null) {
722
  const container = document.getElementById(containerId);
723
  if (!container) {
@@ -809,66 +813,30 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
809
  content.className = `accordion-content px-4 py-3 space-y-3`;
810
  content.id = `content-${solution['category_id']}`;
811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  // Vérifier l'état d'ouverture précédent
813
- const isOpen = accordionStates[solution['category_id']] || false;
814
  console.log(isOpen);
815
  if (!isOpen)
816
  content.classList.add('hidden');
817
 
818
- // Section Problem Description
819
- const problemSection = document.createElement('div');
820
- problemSection.className = 'bg-red-50 border-l-2 border-red-400 p-3 rounded-r-md';
821
- problemSection.innerHTML = `
822
- <h4 class="text-sm font-semibold text-red-800 mb-2 flex items-center">
823
- <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
824
- <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
825
- </svg>
826
- Problem Description
827
- </h4>
828
- <p class="text-xs text-gray-700 leading-relaxed">${solution["problem_description"] || 'Aucune description du problème disponible.'}</p>
829
- `;
830
-
831
- // Section Problem requirements
832
- const reqsSection = document.createElement('div');
833
- reqsSection.className = "bg-gray-50 border-l-2 border-red-400 p-3 rounded-r-md";
834
- const reqItemsUl = solution["requirements"].map(req => `<li>${req}</li>`).join('');
835
- reqsSection.innerHTML = `
836
- <h4 class="text-sm font-semibold text-gray-800 mb-2 flex items-center">
837
- <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
838
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
839
- </svg>
840
- Addressed requirements
841
- </h4>
842
- <ul class="list-disc pl-5 space-y-1 text-gray-700 text-xs">
843
- ${reqItemsUl}
844
- </ul>
845
- `
846
-
847
- // Section Solution Description
848
- const solutionSection = document.createElement('div');
849
- solutionSection.className = 'bg-green-50 border-l-2 border-green-400 p-3 rounded-r-md';
850
- solutionSection.innerHTML = `
851
- <h4 class="text-sm font-semibold text-green-800 mb-2 flex items-center">
852
- <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
853
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
854
- </svg>
855
- Solution Description
856
- </h4>
857
- `;
858
-
859
- // container for markdown content
860
- const solContents = document.createElement('div');
861
- solContents.className = "text-xs text-gray-700 leading-relaxed";
862
- solutionSection.appendChild(solContents);
863
-
864
- try {
865
- solContents.innerHTML = marked.parse(solution['solution_description']);
866
- }
867
- catch (e) {
868
- solContents.innerHTML = `<p class="text-xs text-gray-700 leading-relaxed">${solution['solution_description'] || 'No available solution description'}</p>`;
869
- }
870
-
871
-
872
  // Section Critique
873
  const critiqueSection = document.createElement('div');
874
  critiqueSection.className = 'bg-yellow-50 border-l-2 border-yellow-400 p-3 rounded-r-md';
@@ -918,65 +886,12 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
918
 
919
  critiqueSection.innerHTML = critiqueContent;
920
 
921
- // ===================================== Section sources ================================
922
-
923
- const createEl = (tag, properties) => {
924
- const element = document.createElement(tag);
925
- Object.assign(element, properties);
926
- return element;
927
- };
928
-
929
- // conteneur des sources
930
- const sourcesSection = createEl('div', {
931
- className: 'bg-gray-50 border-l-2 border-gray-400 p-3 rounded-r-md'
932
- });
933
-
934
- const heading = createEl('h4', {
935
- className: 'text-sm font-semibold text-black mb-2 flex items-center',
936
- innerHTML: `
937
- <svg
938
- xmlns="http://www.w3.org/2000/svg"
939
- class="w-4 h-4 mr-1"
940
- fill="none"
941
- viewBox="0 0 24 24"
942
- stroke="currentColor"
943
- stroke-width="2">
944
- <path
945
- stroke-linecap="round"
946
- stroke-linejoin="round"
947
- d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
948
- />
949
- </svg>
950
- Sources
951
- `
952
- });
953
-
954
- const pillContainer = createEl('div', {
955
- className: 'flex flex-wrap mt-1'
956
- });
957
-
958
- // create source reference pills
959
- solution['references'].forEach(source => {
960
- const pillLink = createEl('a', {
961
- href: source.url,
962
- target: '_blank',
963
- rel: 'noopener noreferrer',
964
- className: 'inline-block bg-gray-100 text-black text-xs font-medium mr-2 mb-2 px-3 py-1 rounded-full hover:bg-gray-400 transition-colors',
965
- textContent: source.name
966
- });
967
- pillContainer.appendChild(pillLink);
968
- });
969
-
970
- sourcesSection.append(heading, pillContainer);
971
-
972
  // ======================================================================================
973
 
974
- // Ajouter les sections au contenu
975
- content.appendChild(problemSection);
976
- content.appendChild(reqsSection);
977
- content.appendChild(solutionSection);
978
  content.appendChild(critiqueSection);
979
- content.appendChild(sourcesSection);
980
 
981
  // Événement de clic pour l'accordéon (exclure les boutons de navigation)
982
  header.addEventListener('click', (e) => {
@@ -987,7 +902,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
987
 
988
  // handling open state
989
  const isCurrentlyOpen = !content.classList.contains('hidden');
990
- accordionStates[solution['Category_Id']] = isCurrentlyOpen;
991
  if (isCurrentlyOpen)
992
  content.classList.add('hidden');
993
  else
@@ -1036,20 +951,20 @@ function updateSingleAccordion(solutionCriticizedHistory, containerId, newVersio
1036
  // Fonction d'initialisation simplifiée
1037
  function initializeSolutionAccordion(solutionCriticizedHistory, containerId, startVersion = 0) {
1038
  // Réinitialiser les états d'accordéon
1039
- accordionStates = {};
1040
  createSolutionAccordion(solutionCriticizedHistory, containerId, startVersion);
1041
  document.getElementById(containerId).classList.remove('hidden')
1042
  }
1043
 
1044
  // Supprime toutes les accordéons de solutions générées
1045
  //FIXME: À terme, ne devrait pas exister
1046
- function clearAllSolutions() {
1047
- accordionStates = {}
1048
  solutionsCriticizedVersions = []
1049
  document.querySelectorAll('.solution-accordion').forEach(a => a.remove());
1050
  }
1051
 
1052
- async function generateSolutions(selected_categories, user_constraints = null) {
1053
  console.log(selected_categories);
1054
 
1055
  let input_req = structuredClone(selected_categories);
@@ -1060,21 +975,21 @@ async function generateSolutions(selected_categories, user_constraints = null) {
1060
  return responseObj;
1061
  }
1062
 
1063
- async function generateCriticisms(solutions) {
1064
  let response = await fetch('/solutions/criticize_solution', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(solutions) })
1065
  let responseObj = await response.json()
1066
  solutionsCriticizedVersions.push(responseObj)
1067
  }
1068
 
1069
- async function refineSolutions(critiques) {
1070
  let response = await fetch('/solutions/refine_solutions', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(critiques) })
1071
  let responseObj = await response.json()
1072
- await generateCriticisms(responseObj)
1073
  }
1074
 
1075
- async function workflow(steps = 1) {
1076
  let soluce;
1077
- showLoadingOverlay('Génération des solutions & critiques ....');
1078
 
1079
  const selected_requirements = getSelectedRequirementsByCategory();
1080
  const user_constraints = document.getElementById('additional-gen-instr').value;
@@ -1086,17 +1001,17 @@ async function workflow(steps = 1) {
1086
 
1087
  for (let step = 1; step <= steps; step++) {
1088
  if (requirements_changed) {
1089
- clearAllSolutions();
1090
  console.log("Requirements checksum changed. Cleaning up");
1091
  lastSelectedRequirementsChecksum = selected_requirements.requirements_checksum;
1092
  }
1093
 
1094
  if (solutionsCriticizedVersions.length == 0) {
1095
- soluce = await generateSolutions(selected_requirements, user_constraints ? user_constraints : null);
1096
- await generateCriticisms(soluce)
1097
  } else {
1098
  let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1];
1099
- await refineSolutions(prevSoluce)
1100
  }
1101
  }
1102
  hideLoadingOverlay();
@@ -1108,12 +1023,14 @@ async function workflow(steps = 1) {
1108
  // =============================================================================
1109
 
1110
  document.addEventListener('DOMContentLoaded', function () {
 
1111
  bindTabs();
 
1112
  // Événements des boutons principaux
1113
- // document.getElementById('get-meetings-btn').addEventListener('click', getMeetings);
1114
  document.getElementById('working-group-select').addEventListener('change', (ev) => {
1115
  getMeetings();
1116
  });
 
1117
  document.getElementById('get-tdocs-btn').addEventListener('click', getTDocs);
1118
  document.getElementById('download-tdocs-btn').addEventListener('click', downloadTDocs);
1119
  document.getElementById('extract-requirements-btn').addEventListener('click', extractRequirements);
@@ -1126,14 +1043,20 @@ document.addEventListener('DOMContentLoaded', function () {
1126
  // Événement pour la recherche
1127
  document.getElementById('search-requirements-btn').addEventListener('click', searchRequirements);
1128
 
1129
- // Événements pour les boutons de solutions (à implémenter plus tard)
1130
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1131
  const n_steps = document.getElementById('solution-gen-nsteps').value;
1132
- workflow(n_steps);
1133
  });
1134
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1135
- workflow(1);
1136
  });
 
 
 
 
 
 
1137
  });
1138
 
1139
  // dseactiver le choix du nb de catégories lorsqu'en mode auto
@@ -1145,12 +1068,23 @@ document.getElementById("additional-gen-instr-btn").addEventListener('click', (e
1145
  document.getElementById('additional-gen-instr').focus()
1146
  })
1147
 
 
1148
  document.getElementById('copy-reqs-btn').addEventListener('click', (ev) => {
1149
  copySelectedRequirementsAsMarkdown();
1150
  });
1151
 
 
1152
  document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown);
1153
 
1154
- document.getElementById('test-btn').addEventListener('click', _ => {
1155
- console.log(checkCanUsePrivateGen());
 
 
 
 
 
 
 
 
 
1156
  });
 
1
 
2
  import {
3
  toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect,
4
+ populateCheckboxDropdown, populateDaisyDropdown, extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount,
5
+ bindTabs, checkPrivateLLMInfoAvailable, moveSolutionToDrafts, buildSolutionSubCategories, handleDraftRefine, renderDraftUI, populateLLMModelSelect,
6
+ displayFullAssessment
7
  } from "./ui-utils.js";
8
  import { postWithSSE } from "./sse.js";
9
 
 
15
  let selectedStatus = new Set(); // valeurs cochées (hors "Tous")
16
  let selectedAgenda = new Set();
17
 
18
+ // Requirements
 
19
  let formattedRequirements = [];
20
  let categorizedRequirements = [];
21
+ // les requirements ont ils été extraits au moins une fois ?
22
+ let hasRequirementsExtracted = false;
23
+
24
+ // Generation de solutions
25
+ let solutionAccordionStates = {};
26
  let solutionsCriticizedVersions = [];
27
  // checksum pour vérifier si les requirements séléctionnés ont changé
28
  let lastSelectedRequirementsChecksum = null;
29
+
 
30
 
31
  // =============================================================================
32
  // FONCTIONS MÉTIER
 
431
  const data = await response.json();
432
  categorizedRequirements = data;
433
  displayCategorizedRequirements(categorizedRequirements.categories);
434
+ clearAllBootstrapSolutions();
435
 
436
  // Masquer le container de query et afficher les catégories et boutons solutions
437
  // toggleContainersVisibility(['query-requirements-container'], false);
 
589
  const markdownText = lines.join('\n');
590
 
591
  navigator.clipboard.writeText(markdownText).then(() => {
 
592
  alert("Selected requirements copied to clipboard");
593
  }).catch(err => {
594
  console.error("Failed to copy markdown:", err);
 
720
  container.appendChild(resultsDiv);
721
  }
722
 
723
+ // =========================================== Solution bootstrapping ==============================================
724
+
725
  function createSolutionAccordion(solutionCriticizedHistory, containerId, versionIndex = 0, categoryIndex = null) {
726
  const container = document.getElementById(containerId);
727
  if (!container) {
 
813
  content.className = `accordion-content px-4 py-3 space-y-3`;
814
  content.id = `content-${solution['category_id']}`;
815
 
816
+ // enable solution drafting if private LLM info is present to enable solution private drafting
817
+ if (checkPrivateLLMInfoAvailable()) {
818
+ // Boutons pour passer la solution en draft
819
+ const body_btn_div = document.createElement('div');
820
+ body_btn_div.className = "flex justify-end";
821
+
822
+ const draft_btn = document.createElement('button');
823
+ draft_btn.className = "btn btn-secondary rounded-full";
824
+ draft_btn.innerText = "✏ Draft solution"
825
+ body_btn_div.appendChild(draft_btn);
826
+ content.appendChild(body_btn_div);
827
+
828
+ draft_btn.addEventListener('click', _ => {
829
+ // alert(`Drafting solution ${solution['category_id']} ${versionIndex}`)
830
+ moveSolutionToDrafts(solution);
831
+ });
832
+ }
833
+
834
  // Vérifier l'état d'ouverture précédent
835
+ const isOpen = solutionAccordionStates[solution['category_id']] || false;
836
  console.log(isOpen);
837
  if (!isOpen)
838
  content.classList.add('hidden');
839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  // Section Critique
841
  const critiqueSection = document.createElement('div');
842
  critiqueSection.className = 'bg-yellow-50 border-l-2 border-yellow-400 p-3 rounded-r-md';
 
886
 
887
  critiqueSection.innerHTML = critiqueContent;
888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  // ======================================================================================
890
 
891
+ for (let item of buildSolutionSubCategories(solution))
892
+ content.appendChild(item);
893
+
 
894
  content.appendChild(critiqueSection);
 
895
 
896
  // Événement de clic pour l'accordéon (exclure les boutons de navigation)
897
  header.addEventListener('click', (e) => {
 
902
 
903
  // handling open state
904
  const isCurrentlyOpen = !content.classList.contains('hidden');
905
+ solutionAccordionStates[solution['category_id']] = isCurrentlyOpen;
906
  if (isCurrentlyOpen)
907
  content.classList.add('hidden');
908
  else
 
951
  // Fonction d'initialisation simplifiée
952
  function initializeSolutionAccordion(solutionCriticizedHistory, containerId, startVersion = 0) {
953
  // Réinitialiser les états d'accordéon
954
+ solutionAccordionStates = {};
955
  createSolutionAccordion(solutionCriticizedHistory, containerId, startVersion);
956
  document.getElementById(containerId).classList.remove('hidden')
957
  }
958
 
959
  // Supprime toutes les accordéons de solutions générées
960
  //FIXME: À terme, ne devrait pas exister
961
+ function clearAllBootstrapSolutions() {
962
+ solutionAccordionStates = {}
963
  solutionsCriticizedVersions = []
964
  document.querySelectorAll('.solution-accordion').forEach(a => a.remove());
965
  }
966
 
967
+ async function generateBootstrapSolutions(selected_categories, user_constraints = null) {
968
  console.log(selected_categories);
969
 
970
  let input_req = structuredClone(selected_categories);
 
975
  return responseObj;
976
  }
977
 
978
+ async function generateBootstrapCriticisms(solutions) {
979
  let response = await fetch('/solutions/criticize_solution', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(solutions) })
980
  let responseObj = await response.json()
981
  solutionsCriticizedVersions.push(responseObj)
982
  }
983
 
984
+ async function refineBootstrapSolutions(critiques) {
985
  let response = await fetch('/solutions/refine_solutions', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(critiques) })
986
  let responseObj = await response.json()
987
+ await generateBootstrapCriticisms(responseObj)
988
  }
989
 
990
+ async function boostrapWorkflow(steps = 1) {
991
  let soluce;
992
+ showLoadingOverlay('Boostrapping solutions ....');
993
 
994
  const selected_requirements = getSelectedRequirementsByCategory();
995
  const user_constraints = document.getElementById('additional-gen-instr').value;
 
1001
 
1002
  for (let step = 1; step <= steps; step++) {
1003
  if (requirements_changed) {
1004
+ clearAllBootstrapSolutions();
1005
  console.log("Requirements checksum changed. Cleaning up");
1006
  lastSelectedRequirementsChecksum = selected_requirements.requirements_checksum;
1007
  }
1008
 
1009
  if (solutionsCriticizedVersions.length == 0) {
1010
+ soluce = await generateBootstrapSolutions(selected_requirements, user_constraints ? user_constraints : null);
1011
+ await generateBootstrapCriticisms(soluce)
1012
  } else {
1013
  let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1];
1014
+ await refineBootstrapSolutions(prevSoluce)
1015
  }
1016
  }
1017
  hideLoadingOverlay();
 
1023
  // =============================================================================
1024
 
1025
  document.addEventListener('DOMContentLoaded', function () {
1026
+ // Bind tous les tabs
1027
  bindTabs();
1028
+
1029
  // Événements des boutons principaux
 
1030
  document.getElementById('working-group-select').addEventListener('change', (ev) => {
1031
  getMeetings();
1032
  });
1033
+
1034
  document.getElementById('get-tdocs-btn').addEventListener('click', getTDocs);
1035
  document.getElementById('download-tdocs-btn').addEventListener('click', downloadTDocs);
1036
  document.getElementById('extract-requirements-btn').addEventListener('click', extractRequirements);
 
1043
  // Événement pour la recherche
1044
  document.getElementById('search-requirements-btn').addEventListener('click', searchRequirements);
1045
 
1046
+ // Événements pour les boutons de solutions bootstrappées
1047
  document.getElementById('get-solutions-btn').addEventListener('click', () => {
1048
  const n_steps = document.getElementById('solution-gen-nsteps').value;
1049
+ boostrapWorkflow(n_steps);
1050
  });
1051
  document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
1052
+ boostrapWorkflow(1);
1053
  });
1054
+
1055
+ // Events des boutons pour le drafting de solutions
1056
+ const refineBtn = document.getElementById('refine-btn');
1057
+ refineBtn.addEventListener('click', handleDraftRefine);
1058
+
1059
+ renderDraftUI();
1060
  });
1061
 
1062
  // dseactiver le choix du nb de catégories lorsqu'en mode auto
 
1068
  document.getElementById('additional-gen-instr').focus()
1069
  })
1070
 
1071
+ // copy requirements
1072
  document.getElementById('copy-reqs-btn').addEventListener('click', (ev) => {
1073
  copySelectedRequirementsAsMarkdown();
1074
  });
1075
 
1076
+ // copy all requirements
1077
  document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown);
1078
 
1079
+ // button to fetch llm models
1080
+ document.getElementById('settings-fetch-models').addEventListener('click', _ => {
1081
+ const url = document.getElementById('settings-provider-url').value;
1082
+ const token = document.getElementById('settings-provider-token').value;
1083
+
1084
+ populateLLMModelSelect('settings-provider-model', url, token).catch(e => alert("Error while fetching models: " + e))
1085
+ });
1086
+
1087
+ // button to open full assessment modal
1088
+ document.getElementById('read-assessment-button').addEventListener('click', _ => {
1089
+ displayFullAssessment();
1090
  });
static/js/gen.js ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import zod from 'https://cdn.jsdelivr.net/npm/zod@4.0.10/+esm'
2
+
3
+ /**
4
+ * Met en forme le prompt template passé en paramètres avec les arguments
5
+ * @param {String} template
6
+ * @param {Object} args
7
+ */
8
+ export function formatTemplate(template, args) {
9
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
10
+ // 'match' est la correspondance complète (ex: "{nom}")
11
+ // 'key' est le contenu du premier groupe capturé (ex: "nom")
12
+
13
+ if (key in args)
14
+ return args[key];
15
+
16
+ // Si la clé n'est pas trouvée dans args, on laisse le placeholder tel quel.
17
+ return "";
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Recupère le prompt pour la tâche spécifiée.
23
+ * @param {String} task
24
+ */
25
+ export async function retrieveTemplate(task) {
26
+ const req = await fetch(`/prompt/${task}`)
27
+ return await req.text();
28
+ }
29
+
30
+ /**
31
+ * Lance un deep search sur le serveur pour les topics donnés.
32
+ * @param {Array} topics
33
+ */
34
+ async function performDeepSearch(topics) {
35
+ const response = await fetch('/solutions/search_prior_art', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ topics: topics })
39
+ });
40
+ const results = await response.json();
41
+ }
42
+
43
+ /**
44
+ * Genère une completion avec le LLM specifié
45
+ * @param {String} providerUrl - URL du provider du LLM
46
+ * @param {String} modelName - Nom du modèle à appeler
47
+ * @param {String} apiKey - API key a utiliser
48
+ * @param {Array<{role: string, content: string}>} messages - Liste de messages à passer au modèle
49
+ * @param {Number} temperature - Température à utiliser pour la génération
50
+ */
51
+ export async function generateCompletion(providerUrl, modelName, apiKey, messages, temperature = 0.5) {
52
+ const genEndpoint = providerUrl + "/chat/completions"
53
+
54
+ try {
55
+ const response = await fetch(genEndpoint, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'Authorization': `Bearer ${apiKey}`, // OpenAI-like authorization header
60
+ },
61
+ body: JSON.stringify({
62
+ model: modelName,
63
+ messages: messages,
64
+ temperature: temperature,
65
+ }),
66
+ });
67
+
68
+ if (!response.ok) {
69
+ const errorData = await response.json();
70
+ throw new Error(`API request failed with status ${response.status}: ${errorData.error?.message || 'Unknown error'}`);
71
+ }
72
+
73
+ const data = await response.json();
74
+
75
+ if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content)
76
+ return data.choices[0].message.content;
77
+
78
+ } catch (error) {
79
+ console.error("Error calling private LLM :", error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Genère une completion avec le LLM specifié
86
+ * @param {String} providerUrl - URL du provider du LLM
87
+ * @param {String} modelName - Nom du modèle à appeler
88
+ * @param {String} apiKey - API key a utiliser
89
+ * @param {Array<{role: string, content: string}>} messages - Liste de messages à passer au modèle
90
+ * @param {Object} schema - Zod schema to use for structured generation
91
+ * @param {Number} temperature - Température à utiliser pour la génération
92
+ */
93
+ export async function generateStructuredCompletion(providerUrl, modelName, apiKey, messages, schema, temperature = 0.5) {
94
+ const genEndpoint = providerUrl + "/chat/completions";
95
+ try {
96
+ const response = await fetch(genEndpoint, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'Authorization': `Bearer ${apiKey}`,
101
+ },
102
+ body: JSON.stringify({
103
+ model: modelName,
104
+ messages: messages,
105
+ temperature: temperature,
106
+ response_format: { type: "json_object" }
107
+ }),
108
+ });
109
+
110
+ if (!response.ok) {
111
+ const errorData = await response.json();
112
+ throw new Error(`API request failed with status ${response.status}: ${errorData.error?.message || 'Unknown error'}`);
113
+ }
114
+
115
+ const data = await response.json();
116
+
117
+ console.log(data.choices[0].message.content);
118
+
119
+ // parse json output
120
+ const parsedJSON = JSON.parse(data.choices[0].message.content.replace('```json', '').replace("```", ""));
121
+
122
+ // validate output with zod
123
+ const validatedSchema = schema.parse(parsedJSON);
124
+
125
+ return validatedSchema;
126
+ } catch (error) {
127
+ console.error("Error calling private LLM :", error);
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Retrieves a list of available models from an OpenAI-compatible API using fetch.
134
+ *
135
+ * @param {string} providerUrl The base URL of the OpenAI-compatible API endpoint (e.g., "http://localhost:8000/v1").
136
+ * @param {string} apiKey The API key for authentication.
137
+ * @returns {Promise<Array<string>>} A promise that resolves with an array of model names, or rejects with an error.
138
+ */
139
+ export async function getModelList(providerUrl, apiKey) {
140
+ try {
141
+ // Construct the full URL for the models endpoint
142
+ const modelsUrl = `${providerUrl}/models`;
143
+
144
+ console.log(modelsUrl);
145
+
146
+ // Make a GET request to the models endpoint using fetch
147
+ const response = await fetch(modelsUrl, {
148
+ method: 'GET', // Explicitly state the method
149
+ headers: {
150
+ 'Authorization': `Bearer ${apiKey}`, // OpenAI-compatible authorization header
151
+ 'Content-Type': 'application/json',
152
+ },
153
+ });
154
+
155
+ // Check if the request was successful (status code 200-299)
156
+ if (!response.ok) {
157
+ // If the response is not OK, try to get more error details
158
+ const errorData = await response.json().catch(() => ({})); // Attempt to parse JSON error, fallback to empty object
159
+ throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || response.statusText}`);
160
+ }
161
+
162
+ // Parse the JSON response body
163
+ const data = await response.json();
164
+
165
+ // The response data structure for OpenAI-compatible APIs usually contains a 'data' array
166
+ // where each item represents a model and has an 'id' property.
167
+ if (data && Array.isArray(data.data)) {
168
+ const allModelNames = data.data.map(model => model.id);
169
+
170
+ // Filter out models containing "embedding" (case-insensitive)
171
+ const filteredModelNames = allModelNames.filter(modelName =>
172
+ !modelName.toLowerCase().includes('embedding')
173
+ );
174
+
175
+ return filteredModelNames;
176
+ } else {
177
+ // Handle cases where the response format is unexpected
178
+ throw new Error('Unexpected response format from the API. Could not find model list.');
179
+ }
180
+
181
+ } catch (error) {
182
+ console.error('Error fetching model list:', error.message);
183
+ // Re-throw the error to allow the caller to handle it
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ // # ========================================================================================== Idea assessment logic ==================================================================
189
+
190
+ const StructuredAssessmentOutput = zod.object({
191
+ final_verdict: zod.string(),
192
+ summary: zod.string(),
193
+ insights: zod.array(zod.string()),
194
+ });
195
+
196
+ export async function assessSolution(providerUrl, modelName, apiKey, solution, assessment_rules, portfolio_info) {
197
+ const template = await retrieveTemplate("assess");
198
+
199
+ const assessment_template = formatTemplate(template, {
200
+ notation_criterias: assessment_rules,
201
+ business: portfolio_info,
202
+ problem_description: solution.problem_description,
203
+ solution_description: solution.solution_description,
204
+ });
205
+
206
+ const assessment_full = await generateCompletion(providerUrl, modelName, apiKey, [
207
+ { role: "user", content: assessment_template }
208
+ ]);
209
+
210
+ const structured_template = await retrieveTemplate("extract");
211
+ const structured_filled_template = formatTemplate(structured_template, {
212
+ "report": assessment_full,
213
+ "response_schema": zod.toJSONSchema(StructuredAssessmentOutput)
214
+ })
215
+
216
+ const extracted_info = await generateStructuredCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: structured_filled_template }], StructuredAssessmentOutput);
217
+
218
+ return { assessment_full, extracted_info };
219
+ }
220
+
221
+ export async function refineSolution(providerUrl, modelName, apiKey, solution, insights, assessment_rules, portfolio_info) {
222
+ const template = await retrieveTemplate("refine");
223
+
224
+ const refine_template = formatTemplate(template, {
225
+ "problem_description": solution.problem_description,
226
+ "solution_description": solution.solution_description,
227
+ "insights": insights.map(i => i.text).join("\n -"),
228
+ "business_info": portfolio_info,
229
+ });
230
+
231
+ console.log(refine_template);
232
+
233
+ const refined_idea = await generateCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: refine_template }]);
234
+
235
+ const newSolution = structuredClone(solution);
236
+ newSolution.solution_description = refined_idea;
237
+
238
+ return newSolution;
239
+ }
static/js/ui-utils.js CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  // =============================================================================
2
  // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
3
  // =============================================================================
@@ -212,11 +215,74 @@ export function extractTableData(mapping) {
212
  return data;
213
  }
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  const TABS = {
216
  'doc-table-tab': 'doc-table-tab-contents',
217
  'requirements-tab': 'requirements-tab-contents',
218
  'solutions-tab': 'solutions-tab-contents',
219
- 'query-tab': 'query-tab-contents'
 
220
  };
221
 
222
  /**
@@ -279,16 +345,316 @@ export function debounceAutoCategoryCount(state) {
279
  document.getElementById('category-count').disabled = state;
280
  }
281
 
282
-
283
- /**
284
- * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée.
285
- */
286
- export function checkCanUsePrivateGen() {
287
  const provider_url = document.getElementById('settings-provider-url').value;
288
  const provider_token = document.getElementById('settings-provider-token').value;
 
289
  const assessment_rules = document.getElementById('settings-assessment-rules').value;
290
  const portfolio_info = document.getElementById('settings-portfolio').value;
291
 
 
 
 
 
 
 
 
 
292
  const isEmpty = (str) => (!str?.length);
293
- return !isEmpty(provider_url) && !isEmpty(provider_token) && !isEmpty(assessment_rules) && !isEmpty(portfolio_info);
294
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/16.1.1/lib/marked.esm.js';
2
+ import { assessSolution, getModelList, refineSolution } from "./gen.js"
3
+
4
  // =============================================================================
5
  // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS
6
  // =============================================================================
 
215
  return data;
216
  }
217
 
218
+ /**
219
+ * Construit les sous-catégories communes dans l'affichage des solutions
220
+ */
221
+ export function buildSolutionSubCategories(solution) {
222
+ // Section Problem Description
223
+ const problemSection = document.createElement('div');
224
+ problemSection.className = 'bg-red-50 border-l-2 border-red-400 p-3 rounded-r-md';
225
+ problemSection.innerHTML = `
226
+ <h4 class="text-sm font-semibold text-red-800 mb-2 flex items-center">
227
+ <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
228
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
229
+ </svg>
230
+ Problem Description
231
+ </h4>
232
+ <p class="text-xs text-gray-700 leading-relaxed">${solution["problem_description"] || 'Aucune description du problème disponible.'}</p>
233
+ `;
234
+
235
+ // Section Problem requirements
236
+ const reqsSection = document.createElement('div');
237
+ reqsSection.className = "bg-gray-50 border-l-2 border-red-400 p-3 rounded-r-md";
238
+ const reqItemsUl = solution["requirements"].map(req => `<li>${req}</li>`).join('');
239
+ reqsSection.innerHTML = `
240
+ <h4 class="text-sm font-semibold text-gray-800 mb-2 flex items-center">
241
+ <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
242
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
243
+ </svg>
244
+ Addressed 3GPP requirements
245
+ </h4>
246
+ <ul class="list-disc pl-5 space-y-1 text-gray-700 text-xs">
247
+ ${reqItemsUl}
248
+ </ul>
249
+ `
250
+
251
+ // Section Solution Description
252
+ const solutionSection = document.createElement('div');
253
+ solutionSection.className = 'bg-green-50 border-l-2 border-green-400 p-3 rounded-r-md';
254
+ solutionSection.innerHTML = `
255
+ <h4 class="text-sm font-semibold text-green-800 mb-2 flex items-center">
256
+ <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
257
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
258
+ </svg>
259
+ Solution Description
260
+ </h4>
261
+ `;
262
+
263
+ // container for markdown content
264
+ const solContents = document.createElement('div');
265
+ solContents.className = "text-xs text-gray-700 leading-relaxed";
266
+ solutionSection.appendChild(solContents);
267
+
268
+ try {
269
+ solContents.innerHTML = marked.parse(solution['solution_description']);
270
+ }
271
+ catch (e) {
272
+ console.error(e);
273
+ solContents.innerHTML = `<p class="text-xs text-gray-700 leading-relaxed">${solution['solution_description'] || 'No available solution description'}</p>`;
274
+ }
275
+
276
+ return [problemSection, reqsSection, solutionSection]
277
+ }
278
+
279
+
280
  const TABS = {
281
  'doc-table-tab': 'doc-table-tab-contents',
282
  'requirements-tab': 'requirements-tab-contents',
283
  'solutions-tab': 'solutions-tab-contents',
284
+ 'query-tab': 'query-tab-contents',
285
+ 'draft-tab': 'draft-tab-contents'
286
  };
287
 
288
  /**
 
345
  document.getElementById('category-count').disabled = state;
346
  }
347
 
348
+ export function getPrivateLLMInfo() {
 
 
 
 
349
  const provider_url = document.getElementById('settings-provider-url').value;
350
  const provider_token = document.getElementById('settings-provider-token').value;
351
+ const provider_model = document.getElementById('settings-provider-model').value;
352
  const assessment_rules = document.getElementById('settings-assessment-rules').value;
353
  const portfolio_info = document.getElementById('settings-portfolio').value;
354
 
355
+ return { provider_url, provider_token, provider_model, assessment_rules, portfolio_info };
356
+ }
357
+
358
+ /**
359
+ * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée.
360
+ */
361
+ export function checkPrivateLLMInfoAvailable() {
362
+ const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
363
  const isEmpty = (str) => (!str?.length);
364
+ return !isEmpty(provider_url) && !isEmpty(provider_token) && !isEmpty(assessment_rules) && !isEmpty(portfolio_info) && !isEmpty(provider_model);
365
+ // return true;
366
+ }
367
+
368
+
369
+ /**
370
+ * Populates a select element with model names fetched from the API.
371
+ * @param {string} selectElementId The ID of the HTML select element to populate.
372
+ * @param {string} providerUrl The API provider URL.
373
+ * @param {string} apiKey The API key.
374
+ */
375
+ export async function populateLLMModelSelect(selectElementId, providerUrl, apiKey) {
376
+ const selectElement = document.getElementById(selectElementId);
377
+ if (!selectElement) {
378
+ console.error(`Select element with ID "${selectElementId}" not found.`);
379
+ return;
380
+ }
381
+
382
+ // Clear the "Loading..." option or any existing options
383
+ selectElement.innerHTML = '';
384
+
385
+ try {
386
+ const models = await getModelList(providerUrl, apiKey);
387
+
388
+ if (models.length === 0) {
389
+ const option = document.createElement('option');
390
+ option.value = "";
391
+ option.textContent = "No models found";
392
+ selectElement.appendChild(option);
393
+ selectElement.disabled = true; // Disable if no models
394
+ return;
395
+ }
396
+
397
+ // Add a default "Please select" option
398
+ const defaultOption = document.createElement('option');
399
+ defaultOption.value = ""; // Or a placeholder like "select-model"
400
+ defaultOption.textContent = "Select a model";
401
+ defaultOption.disabled = true; // Make it unselectable initially
402
+ defaultOption.selected = true; // Make it the default selected option
403
+ selectElement.appendChild(defaultOption);
404
+
405
+ // Populate with the fetched models
406
+ models.forEach(modelName => {
407
+ const option = document.createElement('option');
408
+ option.value = modelName;
409
+ option.textContent = modelName;
410
+ selectElement.appendChild(option);
411
+ });
412
+ } catch (error) {
413
+ throw error;
414
+ }
415
+ }
416
+
417
+
418
+
419
+ // ================================================================================ Solution drafting using private LLMs ==========================================================
420
+
421
+ /** History of previously created drafts
422
+ * The draftHistory will look like this:
423
+ * {
424
+ * solution: {} - the solution object
425
+ * insights: [
426
+ * { id: 'i1', text: 'Some insight text', checked: false },
427
+ * { id: 'i2', text: 'Another insight', checked: true }
428
+ * ],
429
+ * assessment_full: The full assessment text
430
+ * }
431
+ */
432
+ let draftHistory = [];
433
+
434
+ // Index of the latest draft in the draft history.
435
+ // -1 means theres no draft.
436
+ let draftCurrentIndex = -1;
437
+
438
+ /**
439
+ * Passe une solution bootstrappée en draft pour être itérée sur le private compute
440
+ * @param {Object} solution - Un objet qui représente une solution bootstrappée (SolutionModel).
441
+ */
442
+ export function moveSolutionToDrafts(solution) {
443
+ const draft_tab_item = document.getElementById('draft-tab');
444
+ if (draft_tab_item.classList.contains("hidden")) // un-hide the draft tab the first time a solution is drafted
445
+ draft_tab_item.classList.remove("hidden");
446
+
447
+ switchTab('draft-tab');
448
+
449
+ const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
450
+
451
+ showLoadingOverlay("Assessing solution ....");
452
+ assessSolution(provider_url, provider_model, provider_token, solution, assessment_rules, portfolio_info).then(response => {
453
+ // reset the state of the draft history
454
+ draftHistory = [];
455
+ draftCurrentIndex = -1;
456
+
457
+ const insights = response.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
458
+
459
+ // push the solution to the draft history
460
+ draftHistory.push({
461
+ solution: solution,
462
+ insights: insights,
463
+ assessment_full: response.assessment_full,
464
+ final_verdict: response.extracted_info.final_verdict,
465
+ assessment_summary: response.extracted_info.summary,
466
+ });
467
+
468
+ draftCurrentIndex++;
469
+ // update the UI by rendering it
470
+ renderDraftUI();
471
+ }).catch(e => {
472
+ alert(e);
473
+ }).finally(() => {
474
+ hideLoadingOverlay();
475
+ })
476
+ }
477
+
478
+ /**
479
+ * SIMULATED API CALL
480
+ * In a real app, this would be an async fetch call to a backend AI/service.
481
+ * @param {string} previousSolution - The text of the previous solution.
482
+ * @param {Array<string>} selectedInsights - An array of the selected insight texts.
483
+ * @returns {object} - An object with the new solution and new insights.
484
+ */
485
+ function assessSolutionDraft(previousSolution, selectedInsights = []) {
486
+ const version = draftHistory.length + 1;
487
+ let newSolutionText;
488
+
489
+ if (selectedInsights.length > 0) {
490
+ newSolutionText = `V${version}: Based on "${previousSolution.substring(0, 30)}..." and the insights: [${selectedInsights.join(', ')}], we've refined the plan to focus more on digital outreach and local partnerships.`;
491
+ } else {
492
+ newSolutionText = `V${version}: This is an initial assessment of "${previousSolution.substring(0, 50)}...". The plan seems solid but could use more detail.`;
493
+ }
494
+
495
+ // Generate some random new insights
496
+ const newInsights = [
497
+ { id: `v${version}-i1`, text: `Consider social media marketing (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
498
+ { id: `v${version}-i2`, text: `Focus on customer retention (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
499
+ { id: `v${version}-i3`, text: `Expand the product line (Insight ${Math.floor(Math.random() * 100)})`, checked: false },
500
+ ];
501
+
502
+ return { newSolutionText, newInsights };
503
+ }
504
+
505
+ /**
506
+ * Renders the timeline UI based on the current state
507
+ * @param {Number} currentIndex - Current index for latest draft
508
+ * @param {Array} drafts - Current history of previous drafts
509
+ */
510
+ function renderDraftTimeline(timelineContainer, currentIndex, drafts) {
511
+ timelineContainer.innerHTML = '';
512
+ drafts.forEach((state, idx) => {
513
+ const li = document.createElement('li');
514
+ li.className = `step ${idx <= currentIndex ? 'step-primary' : ''}`;
515
+ li.textContent = `Draft #${idx + 1}`;
516
+ li.onclick = () => jumpToDraft(idx);
517
+ timelineContainer.appendChild(li);
518
+ });
519
+ }
520
+
521
+ /**
522
+ * Renders the entire UI based on the current state (draftHistory[currentIndex]).
523
+ */
524
+ export function renderDraftUI() {
525
+ const solutionDisplay = document.getElementById('solution-draft-display');
526
+ const insightsContainer = document.getElementById('insights-container');
527
+ const timelineContainer = document.getElementById('timeline-container');
528
+
529
+ if (draftCurrentIndex < 0) {
530
+ solutionDisplay.innerHTML = `<p>No drafted solutions for now</p>`
531
+ insightsContainer.innerHTML = '';
532
+ timelineContainer.innerHTML = '';
533
+ return;
534
+ }
535
+
536
+ const currentState = draftHistory[draftCurrentIndex];
537
+
538
+ const solutionSections = buildSolutionSubCategories(currentState.solution);
539
+ solutionDisplay.innerHTML = '';
540
+
541
+ // 1. Render Solution
542
+ for (let child of solutionSections)
543
+ solutionDisplay.appendChild(child);
544
+
545
+ //3. but 2. actually: Print verdict and quick summary
546
+ document.getElementById('assessment-recommendation-status').innerText = currentState.final_verdict;
547
+ document.getElementById('assessment-recommendation-summary').innerText = currentState.assessment_summary;
548
+
549
+ // 2. Render Insights Checkboxes
550
+ insightsContainer.innerHTML = '';
551
+ currentState.insights.forEach(insight => {
552
+ const isChecked = insight.checked ? 'checked' : '';
553
+ const insightEl = document.createElement('label');
554
+ insightEl.className = 'label cursor-pointer justify-start gap-4';
555
+ insightEl.innerHTML = `
556
+ <input type="checkbox" id="${insight.id}" ${isChecked} class="checkbox checkbox-primary" />
557
+ <span class="label-text">${insight.text}</span>
558
+ `;
559
+ // Add event listener to update state on check/uncheck
560
+ insightEl.querySelector('input').addEventListener('change', (e) => {
561
+ insight.checked = e.target.checked;
562
+ });
563
+ insightsContainer.appendChild(insightEl);
564
+ });
565
+
566
+
567
+ // Render the timeline with the fetched timeline container
568
+ renderDraftTimeline(timelineContainer, draftCurrentIndex, draftHistory);
569
+
570
+ console.log(draftHistory);
571
+ console.log(draftCurrentIndex);
572
+ }
573
+
574
+ /**
575
+ * Handles the "Refine" button click.
576
+ */
577
+ export function handleDraftRefine() {
578
+ // Fetch DOM elements here
579
+ const refineBtn = document.getElementById('refine-btn');
580
+
581
+ const currentState = draftHistory[draftCurrentIndex];
582
+
583
+ // Get selected insights from the current state
584
+ const selectedInsights = currentState.insights
585
+ .filter(i => i.checked)
586
+ .map(i => i.text);
587
+
588
+ if (selectedInsights.length === 0) {
589
+ alert('Please select at least one insight to refine the solution.');
590
+ return;
591
+ }
592
+
593
+ // --- THIS IS THE KEY LOGIC FOR INVALIDATING THE FUTURE ---
594
+ // If we are not at the end of the timeline, chop off the future states.
595
+ if (draftCurrentIndex < draftHistory.length - 1) {
596
+ draftHistory = draftHistory.slice(0, draftCurrentIndex + 1);
597
+ }
598
+ // ---
599
+
600
+ const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo();
601
+
602
+ showLoadingOverlay('Refining and assessing ....')
603
+
604
+ refineSolution(provider_url, provider_model, provider_token, currentState.solution, selectedInsights, assessment_rules, portfolio_info)
605
+ .then(newSolution => {
606
+ const refinedSolution = newSolution;
607
+ return assessSolution(provider_url, provider_model, provider_token, newSolution, assessment_rules, portfolio_info)
608
+ .then(assessedResult => {
609
+ return { refinedSolution, assessedResult };
610
+ });
611
+ })
612
+ .then(result => {
613
+ const newInsights = result.assessedResult.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false }));
614
+
615
+ draftHistory.push({
616
+ solution: result.refinedSolution,
617
+ insights: newInsights,
618
+ assessment_full: result.assessedResult.assessment_full,
619
+ final_verdict: result.assessedResult.extracted_info.final_verdict,
620
+ assessment_summary: result.assessedResult.extracted_info.summary,
621
+ });
622
+
623
+ draftCurrentIndex++;
624
+ renderDraftUI();
625
+ })
626
+ .catch(error => {
627
+ // Handle any errors
628
+ alert("An error occurred:" + error);
629
+ }).finally(() => {
630
+ hideLoadingOverlay();
631
+ });
632
+ }
633
+
634
+ /**
635
+ * Jumps to a specific state in the draftHistory timeline.
636
+ */
637
+ function jumpToDraft(index) {
638
+ if (index >= 0 && index < draftHistory.length) {
639
+ draftCurrentIndex = index;
640
+ renderDraftUI();
641
+ }
642
+ }
643
+
644
+ export function displayFullAssessment() {
645
+ const full_assessment_content = document.getElementById('read-assessment-content');
646
+ const modal = document.getElementById('read-assessment-modal');
647
+
648
+ if (draftCurrentIndex < 0)
649
+ return;
650
+
651
+ const lastDraft = draftHistory[draftCurrentIndex];
652
+ try {
653
+ full_assessment_content.innerHTML = marked.parse(lastDraft.assessment_full);
654
+ }
655
+ catch (e) {
656
+ full_assessment_content.innerHTML = lastDraft.assessment_full;
657
+ }
658
+
659
+ modal.showModal();
660
+ }