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

Reword routes + prepare for private gen

Browse files
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, _SearchedSolutionModel, _SolutionCriticismOutput, CriticizeSolutionsRequest, CritiqueResponse, InsightFinderConstraintsList, ReqGroupingCategory, ReqGroupingRequest, ReqGroupingResponse, ReqSearchLLMResponse, ReqSearchRequest, ReqSearchResponse, SolutionCriticism, SolutionModel, SolutionSearchResponse, SolutionSearchV2Request, TechnologyData
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
- async def _search_solution_inner(cat: ReqGroupingCategory):
 
 
 
 
 
 
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": _SearchedSolutionModel.model_json_schema()
71
  })}
72
- ], response_format=_SearchedSolutionModel)
73
 
74
- format_solution_model = _SearchedSolutionModel.model_validate_json(
75
  format_solution.choices[0].message.content)
76
 
77
  final_solution = SolutionModel(
78
- Context="",
79
- Requirements=[
80
  cat.requirements[i].requirement for i in format_solution_model.requirement_ids
81
  ],
82
- Problem_Description=format_solution_model.problem_description,
83
- Solution_Description=format_solution_model.solution_description,
84
- References=[],
85
- Category_Id=cat.id,
86
  )
87
 
88
  # ========================================================================================================================================
89
 
90
  return final_solution
91
 
92
- tasks = await asyncio.gather(*[_search_solution_inner(cat) for cat in req.categories], return_exceptions=True)
93
  final_solutions = [sol for sol in tasks if not isinstance(sol, Exception)]
94
 
95
- return SolutionSearchResponse(solutions=final_solutions)
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=SolutionSearchResponse)
127
- async def refine_solutions(params: CritiqueResponse, prompt_env: Environment = Depends(get_prompt_templates), llm_router: Router = Depends(get_llm_router)) -> SolutionSearchResponse:
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.Problem_Description = req_model.problem_description
147
- refined_solution.Solution_Description = req_model.solution_description
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 SolutionSearchResponse(solutions=refined_solutions)
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["Context"]}}
12
- - Problem description: {{solution["Problem_Description"]}}
13
- - Solution description: {{solution["Solution_Description"]}}
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['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:
 
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
- Context: str = Field(...,
75
  description="Full context provided for this category.")
76
- Requirements: List[str] = Field(...,
77
- description="List of each requirement as string.")
78
- Problem_Description: str = Field(..., alias="Problem Description",
79
  description="Description of the problem being solved.")
80
- Solution_Description: str = Field(..., alias="Solution Description",
81
  description="Detailed description of the solution.")
82
- References: list[dict] = Field(
83
  ..., description="References to documents used for the solution.")
84
- Category_Id: int = Field(
 
85
  ..., description="ID of the requirements category the solution is based on")
86
 
87
- class Config:
88
- validate_by_name = True # Enables alias handling on input/output
89
 
90
 
91
  # ============================================================= Categorize requirements endpoint
@@ -115,14 +125,14 @@ class _ReqGroupingOutput(BaseModel):
115
  ..., description="List of grouping categories")
116
 
117
 
118
- # =================================================================== search solution response endpoint
119
 
120
- class _SolutionSearchOutput(BaseModel):
121
  solution: SolutionModel
122
 
123
 
124
- class _SearchedSolutionModel(BaseModel):
125
- """"Internal model used for solutions searched using gemini"""
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 SolutionSearchResponse(BaseModel):
135
- """Response model for solution search"""
136
  solutions: list[SolutionModel]
137
 
138
 
139
- class SolutionSearchV2Request(BaseModel):
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['Category_Id']).title;
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['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,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['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,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['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,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['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,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['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,10 +862,10 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
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,7 +956,7 @@ function createSingleAccordionItem(item, index, versionIndex, solutionCriticized
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,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/search_solutions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) })
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
  }