Lucas ARRIESSE
commited on
Commit
·
75ac1c4
1
Parent(s):
e4ce2a0
Reword routes + prepare for private gen
Browse files- api/solutions.py +24 -21
- prompts/criticize.txt +3 -3
- prompts/refine_solution.txt +4 -4
- schemas.py +26 -17
- static/js/app.js +12 -12
api/solutions.py
CHANGED
@@ -7,7 +7,7 @@ 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,
|
11 |
|
12 |
# Router for solution generation and critique
|
13 |
router = APIRouter(tags=["solution generation and critique"])
|
@@ -34,10 +34,14 @@ async def retry_until(
|
|
34 |
|
35 |
# =================================================== Search solutions ============================================================================
|
36 |
|
37 |
-
@router.post("/search_solutions")
|
38 |
-
async def search_solutions_if(req: SolutionSearchV2Request, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router), http_client: AsyncClient = Depends(get_http_client)) -> SolutionSearchResponse:
|
39 |
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
# process requirements into insight finder format
|
42 |
fmt_completion = await llm_router.acompletion("gemini-v2", messages=[
|
43 |
{
|
@@ -67,32 +71,32 @@ async def search_solutions_if(req: SolutionSearchV2Request, prompt_env: Environm
|
|
67 |
"category": cat.model_dump(),
|
68 |
"technologies": technologies.model_dump()["technologies"],
|
69 |
"user_constraints": req.user_constraints,
|
70 |
-
"response_schema":
|
71 |
})}
|
72 |
-
], response_format=
|
73 |
|
74 |
-
format_solution_model =
|
75 |
format_solution.choices[0].message.content)
|
76 |
|
77 |
final_solution = SolutionModel(
|
78 |
-
|
79 |
-
|
80 |
cat.requirements[i].requirement for i in format_solution_model.requirement_ids
|
81 |
],
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
)
|
87 |
|
88 |
# ========================================================================================================================================
|
89 |
|
90 |
return final_solution
|
91 |
|
92 |
-
tasks = await asyncio.gather(*[
|
93 |
final_solutions = [sol for sol in tasks if not isinstance(sol, Exception)]
|
94 |
|
95 |
-
return
|
96 |
|
97 |
|
98 |
@router.post("/criticize_solution", response_model=CritiqueResponse)
|
@@ -123,8 +127,8 @@ async def criticize_solution(params: CriticizeSolutionsRequest, prompt_env: Envi
|
|
123 |
|
124 |
# =================================================================== Refine solution ====================================
|
125 |
|
126 |
-
@router.post("/refine_solutions", response_model=
|
127 |
-
async def refine_solutions(params: CritiqueResponse, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router)) ->
|
128 |
"""Refines the previously critiqued solutions."""
|
129 |
|
130 |
async def __refine_solution(crit: SolutionCriticism):
|
@@ -143,12 +147,11 @@ async def refine_solutions(params: CritiqueResponse, prompt_env: Environment = D
|
|
143 |
|
144 |
# copy previous solution model
|
145 |
refined_solution = crit.solution.model_copy(deep=True)
|
146 |
-
refined_solution.
|
147 |
-
refined_solution.
|
148 |
|
149 |
return refined_solution
|
150 |
|
151 |
refined_solutions = await asyncio.gather(*[__refine_solution(crit) for crit in params.critiques], return_exceptions=False)
|
152 |
|
153 |
-
return
|
154 |
-
|
|
|
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"])
|
|
|
34 |
|
35 |
# =================================================== Search solutions ============================================================================
|
36 |
|
|
|
|
|
37 |
|
38 |
+
@router.post("/bootstrap_solutions")
|
39 |
+
async def bootstrap_solutions(req: SolutionBootstrapRequest, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router), http_client: AsyncClient = Depends(get_http_client)) -> SolutionBootstrapResponse:
|
40 |
+
"""
|
41 |
+
Boostraps a solution for each of the passed in requirements categories using Insight Finder's API.
|
42 |
+
"""
|
43 |
+
|
44 |
+
async def _bootstrap_solution_inner(cat: ReqGroupingCategory):
|
45 |
# process requirements into insight finder format
|
46 |
fmt_completion = await llm_router.acompletion("gemini-v2", messages=[
|
47 |
{
|
|
|
71 |
"category": cat.model_dump(),
|
72 |
"technologies": technologies.model_dump()["technologies"],
|
73 |
"user_constraints": req.user_constraints,
|
74 |
+
"response_schema": _BootstrappedSolutionModel.model_json_schema()
|
75 |
})}
|
76 |
+
], response_format=_BootstrappedSolutionModel)
|
77 |
|
78 |
+
format_solution_model = _BootstrappedSolutionModel.model_validate_json(
|
79 |
format_solution.choices[0].message.content)
|
80 |
|
81 |
final_solution = SolutionModel(
|
82 |
+
context="",
|
83 |
+
requirements=[
|
84 |
cat.requirements[i].requirement for i in format_solution_model.requirement_ids
|
85 |
],
|
86 |
+
problem_description=format_solution_model.problem_description,
|
87 |
+
solution_description=format_solution_model.solution_description,
|
88 |
+
references=[],
|
89 |
+
category_id=cat.id,
|
90 |
)
|
91 |
|
92 |
# ========================================================================================================================================
|
93 |
|
94 |
return final_solution
|
95 |
|
96 |
+
tasks = await asyncio.gather(*[_bootstrap_solution_inner(cat) for cat in req.categories], return_exceptions=True)
|
97 |
final_solutions = [sol for sol in tasks if not isinstance(sol, Exception)]
|
98 |
|
99 |
+
return SolutionBootstrapResponse(solutions=final_solutions)
|
100 |
|
101 |
|
102 |
@router.post("/criticize_solution", response_model=CritiqueResponse)
|
|
|
127 |
|
128 |
# =================================================================== Refine solution ====================================
|
129 |
|
130 |
+
@router.post("/refine_solutions", response_model=SolutionBootstrapResponse)
|
131 |
+
async def refine_solutions(params: CritiqueResponse, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router)) -> SolutionBootstrapResponse:
|
132 |
"""Refines the previously critiqued solutions."""
|
133 |
|
134 |
async def __refine_solution(crit: SolutionCriticism):
|
|
|
147 |
|
148 |
# copy previous solution model
|
149 |
refined_solution = crit.solution.model_copy(deep=True)
|
150 |
+
refined_solution.problem_description = req_model.problem_description
|
151 |
+
refined_solution.solution_description = req_model.solution_description
|
152 |
|
153 |
return refined_solution
|
154 |
|
155 |
refined_solutions = await asyncio.gather(*[__refine_solution(crit) for crit in params.critiques], return_exceptions=False)
|
156 |
|
157 |
+
return SolutionBootstrapResponse(solutions=refined_solutions)
|
|
prompts/criticize.txt
CHANGED
@@ -8,9 +8,9 @@ Here are the solutions:
|
|
8 |
<solutions>
|
9 |
{% for solution in solutions %}
|
10 |
## Solution
|
11 |
-
- Context: {{solution["
|
12 |
-
- Problem description: {{solution["
|
13 |
-
- Solution description: {{solution["
|
14 |
---
|
15 |
{% endfor -%}
|
16 |
</solutions>
|
|
|
8 |
<solutions>
|
9 |
{% for solution in solutions %}
|
10 |
## Solution
|
11 |
+
- Context: {{solution["context"]}}
|
12 |
+
- Problem description: {{solution["problem_description"]}}
|
13 |
+
- Solution description: {{solution["solution_description"]}}
|
14 |
---
|
15 |
{% endfor -%}
|
16 |
</solutions>
|
prompts/refine_solution.txt
CHANGED
@@ -7,18 +7,18 @@ No need to include that the solution is refined.
|
|
7 |
Here is the solution:
|
8 |
<solution>
|
9 |
# Solution Context:
|
10 |
-
{{solution['
|
11 |
|
12 |
# Requirements solved by the solution
|
13 |
-
{% for req in solution['
|
14 |
- {{req}}
|
15 |
{% endfor %}
|
16 |
|
17 |
# Problem description associated to the solution
|
18 |
-
{{solution['
|
19 |
|
20 |
# Description of the solution
|
21 |
-
{{solution['
|
22 |
</solution>
|
23 |
|
24 |
Here is the criticism:
|
|
|
7 |
Here is the solution:
|
8 |
<solution>
|
9 |
# Solution Context:
|
10 |
+
{{solution['context']}}
|
11 |
|
12 |
# Requirements solved by the solution
|
13 |
+
{% for req in solution['requirements'] -%}
|
14 |
- {{req}}
|
15 |
{% endfor %}
|
16 |
|
17 |
# Problem description associated to the solution
|
18 |
+
{{solution['problem_description']}}
|
19 |
|
20 |
# Description of the solution
|
21 |
+
{{solution['solution_description']}}
|
22 |
</solution>
|
23 |
|
24 |
Here is the criticism:
|
schemas.py
CHANGED
@@ -1,39 +1,48 @@
|
|
1 |
from pydantic import BaseModel, Field
|
2 |
from typing import Any, List, Dict, Optional
|
3 |
|
|
|
4 |
class MeetingsRequest(BaseModel):
|
5 |
working_group: str
|
6 |
|
|
|
7 |
class MeetingsResponse(BaseModel):
|
8 |
meetings: Dict[str, str]
|
9 |
# --------------------------------------
|
10 |
|
|
|
11 |
class DataRequest(BaseModel):
|
12 |
working_group: str
|
13 |
meeting: str
|
14 |
|
|
|
15 |
class DataResponse(BaseModel):
|
16 |
data: List[Dict[Any, Any]]
|
17 |
|
18 |
# --------------------------------------
|
19 |
|
|
|
20 |
class DocInfo(BaseModel):
|
21 |
document: str
|
22 |
url: str
|
23 |
|
|
|
24 |
class RequirementsRequest(BaseModel):
|
25 |
documents: List[DocInfo]
|
26 |
|
|
|
27 |
class DocRequirements(BaseModel):
|
28 |
document: str
|
29 |
context: str
|
30 |
requirements: List[str]
|
31 |
|
|
|
32 |
class RequirementsResponse(BaseModel):
|
33 |
requirements: List[DocRequirements]
|
34 |
|
35 |
# --------------------------------------
|
36 |
|
|
|
37 |
class RequirementInfo(BaseModel):
|
38 |
req_id: int = Field(..., description="The ID of this requirement")
|
39 |
context: str = Field(..., description="Context for the requirement.")
|
@@ -71,21 +80,22 @@ class ReqGroupingCategory(BaseModel):
|
|
71 |
|
72 |
|
73 |
class SolutionModel(BaseModel):
|
74 |
-
|
75 |
description="Full context provided for this category.")
|
76 |
-
|
77 |
-
description="List of each requirement as string.")
|
78 |
-
|
79 |
description="Description of the problem being solved.")
|
80 |
-
|
81 |
description="Detailed description of the solution.")
|
82 |
-
|
83 |
..., description="References to documents used for the solution.")
|
84 |
-
|
|
|
85 |
..., description="ID of the requirements category the solution is based on")
|
86 |
|
87 |
-
class Config:
|
88 |
-
|
89 |
|
90 |
|
91 |
# ============================================================= Categorize requirements endpoint
|
@@ -115,14 +125,14 @@ class _ReqGroupingOutput(BaseModel):
|
|
115 |
..., description="List of grouping categories")
|
116 |
|
117 |
|
118 |
-
# ===================================================================
|
119 |
|
120 |
-
class
|
121 |
solution: SolutionModel
|
122 |
|
123 |
|
124 |
-
class
|
125 |
-
""""Internal model used for solutions
|
126 |
requirement_ids: List[int] = Field(...,
|
127 |
description="List of each requirement ID addressed by the solution")
|
128 |
problem_description: str = Field(...,
|
@@ -131,13 +141,12 @@ class _SearchedSolutionModel(BaseModel):
|
|
131 |
description="Detailed description of the solution.")
|
132 |
|
133 |
|
134 |
-
class
|
135 |
-
"""Response model for solution
|
136 |
solutions: list[SolutionModel]
|
137 |
|
138 |
|
139 |
-
class
|
140 |
-
"""Response of a requirement grouping call."""
|
141 |
categories: List[ReqGroupingCategory]
|
142 |
user_constraints: Optional[str] = Field(
|
143 |
default=None, description="Additional user constraints to respect when generating the solutions.")
|
|
|
1 |
from pydantic import BaseModel, Field
|
2 |
from typing import Any, List, Dict, Optional
|
3 |
|
4 |
+
|
5 |
class MeetingsRequest(BaseModel):
|
6 |
working_group: str
|
7 |
|
8 |
+
|
9 |
class MeetingsResponse(BaseModel):
|
10 |
meetings: Dict[str, str]
|
11 |
# --------------------------------------
|
12 |
|
13 |
+
|
14 |
class DataRequest(BaseModel):
|
15 |
working_group: str
|
16 |
meeting: str
|
17 |
|
18 |
+
|
19 |
class DataResponse(BaseModel):
|
20 |
data: List[Dict[Any, Any]]
|
21 |
|
22 |
# --------------------------------------
|
23 |
|
24 |
+
|
25 |
class DocInfo(BaseModel):
|
26 |
document: str
|
27 |
url: str
|
28 |
|
29 |
+
|
30 |
class RequirementsRequest(BaseModel):
|
31 |
documents: List[DocInfo]
|
32 |
|
33 |
+
|
34 |
class DocRequirements(BaseModel):
|
35 |
document: str
|
36 |
context: str
|
37 |
requirements: List[str]
|
38 |
|
39 |
+
|
40 |
class RequirementsResponse(BaseModel):
|
41 |
requirements: List[DocRequirements]
|
42 |
|
43 |
# --------------------------------------
|
44 |
|
45 |
+
|
46 |
class RequirementInfo(BaseModel):
|
47 |
req_id: int = Field(..., description="The ID of this requirement")
|
48 |
context: str = Field(..., description="Context for the requirement.")
|
|
|
80 |
|
81 |
|
82 |
class SolutionModel(BaseModel):
|
83 |
+
context: str = Field(...,
|
84 |
description="Full context provided for this category.")
|
85 |
+
requirements: List[str] = Field(...,
|
86 |
+
description="List of each requirement covered by the solution as a string.")
|
87 |
+
problem_description: str = Field(...,
|
88 |
description="Description of the problem being solved.")
|
89 |
+
solution_description: str = Field(...,
|
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 |
|
97 |
+
# class Config:
|
98 |
+
# validate_by_name = True # Enables alias handling on input/output
|
99 |
|
100 |
|
101 |
# ============================================================= Categorize requirements endpoint
|
|
|
125 |
..., description="List of grouping categories")
|
126 |
|
127 |
|
128 |
+
# =================================================================== bootstrap solution response
|
129 |
|
130 |
+
class _SolutionBootstrapOutput(BaseModel):
|
131 |
solution: SolutionModel
|
132 |
|
133 |
|
134 |
+
class _BootstrappedSolutionModel(BaseModel):
|
135 |
+
""""Internal model used for solutions bootstrapped using """
|
136 |
requirement_ids: List[int] = Field(...,
|
137 |
description="List of each requirement ID addressed by the solution")
|
138 |
problem_description: str = Field(...,
|
|
|
141 |
description="Detailed description of the solution.")
|
142 |
|
143 |
|
144 |
+
class SolutionBootstrapResponse(BaseModel):
|
145 |
+
"""Response model for solution bootstrapping"""
|
146 |
solutions: list[SolutionModel]
|
147 |
|
148 |
|
149 |
+
class SolutionBootstrapRequest(BaseModel):
|
|
|
150 |
categories: List[ReqGroupingCategory]
|
151 |
user_constraints: Optional[str] = Field(
|
152 |
default=None, description="Additional user constraints to respect when generating the solutions.")
|
static/js/app.js
CHANGED
@@ -756,7 +756,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
756 |
const criticism = item.criticism;
|
757 |
|
758 |
// Récupérer le titre de la catégorie
|
759 |
-
const categoryTitle = categorizedRequirements.categories.find(c => c.id == solution['
|
760 |
|
761 |
// Container pour chaque solution
|
762 |
const solutionCard = document.createElement('div');
|
@@ -777,7 +777,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
777 |
<h3 class="text-sm font-semibold text-gray-800">${categoryTitle}</h3>
|
778 |
<div class="flex items-center space-x-2 bg-white px-3 py-1 rounded-full border">
|
779 |
<button class="version-btn-left w-6 h-6 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors ${currentVersion === 1 ? 'opacity-50 cursor-not-allowed' : ''}"
|
780 |
-
data-solution-index="${solution['
|
781 |
${currentVersion === 1 ? 'disabled' : ''}>
|
782 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
783 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
@@ -785,7 +785,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
785 |
</button>
|
786 |
<span class="text-xs font-medium text-gray-600 min-w-[60px] text-center version-indicator">Version ${currentVersion}</span>
|
787 |
<button class="version-btn-right w-6 h-6 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors ${currentVersion === totalVersions ? 'opacity-50 cursor-not-allowed' : ''}"
|
788 |
-
data-solution-index="${solution['
|
789 |
${currentVersion === totalVersions ? 'disabled' : ''}>
|
790 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
791 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
@@ -795,7 +795,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
795 |
</div>
|
796 |
<!--
|
797 |
<button class="delete-btn text-red-500 hover:text-red-700 transition-colors"
|
798 |
-
data-solution-index="${solution['
|
799 |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
800 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5-3h4m-4 0a1 1 0 00-1 1v1h6V5a1 1 0 00-1-1m-4 0h4" />
|
801 |
</svg>
|
@@ -807,10 +807,10 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
807 |
// Contenu de l'accordéon
|
808 |
const content = document.createElement('div');
|
809 |
content.className = `accordion-content px-4 py-3 space-y-3`;
|
810 |
-
content.id = `content-${solution['
|
811 |
|
812 |
// Vérifier l'état d'ouverture précédent
|
813 |
-
const isOpen = accordionStates[solution['
|
814 |
console.log(isOpen);
|
815 |
if (!isOpen)
|
816 |
content.classList.add('hidden');
|
@@ -825,13 +825,13 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
825 |
</svg>
|
826 |
Problem Description
|
827 |
</h4>
|
828 |
-
<p class="text-xs text-gray-700 leading-relaxed">${solution[
|
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["
|
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">
|
@@ -862,10 +862,10 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
862 |
solutionSection.appendChild(solContents);
|
863 |
|
864 |
try {
|
865 |
-
solContents.innerHTML = marked.parse(solution['
|
866 |
}
|
867 |
catch (e) {
|
868 |
-
solContents.innerHTML = `<p class="text-xs text-gray-700 leading-relaxed">${solution['
|
869 |
}
|
870 |
|
871 |
|
@@ -956,7 +956,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
|
|
956 |
});
|
957 |
|
958 |
// create source reference pills
|
959 |
-
solution['
|
960 |
const pillLink = createEl('a', {
|
961 |
href: source.url,
|
962 |
target: '_blank',
|
@@ -1055,7 +1055,7 @@ async function generateSolutions(selected_categories, user_constraints = null) {
|
|
1055 |
let input_req = structuredClone(selected_categories);
|
1056 |
input_req.user_constraints = user_constraints;
|
1057 |
|
1058 |
-
let response = await fetch("/solutions/
|
1059 |
let responseObj = await response.json()
|
1060 |
return responseObj;
|
1061 |
}
|
|
|
756 |
const criticism = item.criticism;
|
757 |
|
758 |
// Récupérer le titre de la catégorie
|
759 |
+
const categoryTitle = categorizedRequirements.categories.find(c => c.id == solution['category_id']).title;
|
760 |
|
761 |
// Container pour chaque solution
|
762 |
const solutionCard = document.createElement('div');
|
|
|
777 |
<h3 class="text-sm font-semibold text-gray-800">${categoryTitle}</h3>
|
778 |
<div class="flex items-center space-x-2 bg-white px-3 py-1 rounded-full border">
|
779 |
<button class="version-btn-left w-6 h-6 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors ${currentVersion === 1 ? 'opacity-50 cursor-not-allowed' : ''}"
|
780 |
+
data-solution-index="${solution['category_id']}"
|
781 |
${currentVersion === 1 ? 'disabled' : ''}>
|
782 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
783 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
|
785 |
</button>
|
786 |
<span class="text-xs font-medium text-gray-600 min-w-[60px] text-center version-indicator">Version ${currentVersion}</span>
|
787 |
<button class="version-btn-right w-6 h-6 flex items-center justify-center rounded-full hover:bg-gray-100 transition-colors ${currentVersion === totalVersions ? 'opacity-50 cursor-not-allowed' : ''}"
|
788 |
+
data-solution-index="${solution['category_id']}"
|
789 |
${currentVersion === totalVersions ? 'disabled' : ''}>
|
790 |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
791 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
|
795 |
</div>
|
796 |
<!--
|
797 |
<button class="delete-btn text-red-500 hover:text-red-700 transition-colors"
|
798 |
+
data-solution-index="${solution['category_id']}" id="solution-delete-btn">
|
799 |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
800 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5-3h4m-4 0a1 1 0 00-1 1v1h6V5a1 1 0 00-1-1m-4 0h4" />
|
801 |
</svg>
|
|
|
807 |
// Contenu de l'accordéon
|
808 |
const content = document.createElement('div');
|
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');
|
|
|
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">
|
|
|
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 |
|
|
|
956 |
});
|
957 |
|
958 |
// create source reference pills
|
959 |
+
solution['references'].forEach(source => {
|
960 |
const pillLink = createEl('a', {
|
961 |
href: source.url,
|
962 |
target: '_blank',
|
|
|
1055 |
let input_req = structuredClone(selected_categories);
|
1056 |
input_req.user_constraints = user_constraints;
|
1057 |
|
1058 |
+
let response = await fetch("/solutions/bootstrap_solutions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) })
|
1059 |
let responseObj = await response.json()
|
1060 |
return responseObj;
|
1061 |
}
|