Lucas ARRIESSE
commited on
Commit
·
e97be0e
1
Parent(s):
75ac1c4
WIP solution drafting
Browse files- README.md +8 -0
- api/requirements.py +3 -2
- api/solutions.py +41 -4
- app.py +17 -3
- prompts/{synthesize_solution.txt → bootstrap_solution.txt} +0 -1
- prompts/private/README +1 -0
- prompts/private/assess.txt +25 -0
- prompts/private/extract.txt +24 -0
- prompts/private/refine.txt +24 -0
- prompts/search/build_final_report.txt +57 -0
- prompts/search/search_topic.txt +12 -0
- schemas.py +36 -29
- static/index.html +98 -15
- static/js/app.js +74 -140
- static/js/gen.js +239 -0
- static/js/ui-utils.js +374 -8
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("
|
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 |
-
|
|
|
|
|
|
|
195 |
|
196 |
-
# Helpers to extract constraints for Insights Finder
|
197 |
|
198 |
-
class
|
199 |
-
|
200 |
-
|
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 |
-
|
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"
|
85 |
-
|
86 |
-
<a role="tab" class="tab tab-disabled" id="
|
87 |
-
|
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">
|
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 |
-
<
|
|
|
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 |
-
</
|
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,
|
5 |
-
|
6 |
-
|
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 |
-
//
|
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 |
-
|
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 |
-
|
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 =
|
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 |
-
|
975 |
-
|
976 |
-
|
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 |
-
|
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 |
-
|
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
|
1047 |
-
|
1048 |
solutionsCriticizedVersions = []
|
1049 |
document.querySelectorAll('.solution-accordion').forEach(a => a.remove());
|
1050 |
}
|
1051 |
|
1052 |
-
async function
|
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
|
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
|
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
|
1073 |
}
|
1074 |
|
1075 |
-
async function
|
1076 |
let soluce;
|
1077 |
-
showLoadingOverlay('
|
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 |
-
|
1090 |
console.log("Requirements checksum changed. Cleaning up");
|
1091 |
lastSelectedRequirementsChecksum = selected_requirements.requirements_checksum;
|
1092 |
}
|
1093 |
|
1094 |
if (solutionsCriticizedVersions.length == 0) {
|
1095 |
-
soluce = await
|
1096 |
-
await
|
1097 |
} else {
|
1098 |
let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1];
|
1099 |
-
await
|
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
|
1130 |
document.getElementById('get-solutions-btn').addEventListener('click', () => {
|
1131 |
const n_steps = document.getElementById('solution-gen-nsteps').value;
|
1132 |
-
|
1133 |
});
|
1134 |
document.getElementById('get-solutions-step-btn').addEventListener('click', () => {
|
1135 |
-
|
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 |
-
|
1155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/[email protected]/+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 |
+
}
|