Spaces:
Sleeping
Sleeping
import { | |
toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect, | |
populateCheckboxDropdown, populateDaisyDropdown, extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount, | |
bindTabs, checkPrivateLLMInfoAvailable, moveSolutionToDrafts, buildSolutionSubCategories, handleDraftRefine, renderDraftUI, populateLLMModelSelect, | |
displayFullAssessment, handleSaveConfigFields, handleLoadConfigFields, handleFTOAnalysis, handleClearConfig | |
} from "./ui.js"; | |
import { postWithSSE } from "./sse.js"; | |
// ==================================== Variables globales ======================================== | |
let requirements = []; | |
// Filtres | |
let selectedType = ""; // "" = Tous | |
let selectedStatus = new Set(); // valeurs cochées (hors "Tous") | |
let selectedAgenda = new Set(); | |
// Requirements | |
let formattedRequirements = []; | |
let categorizedRequirements = []; | |
// les requirements ont ils été extraits au moins une fois ? | |
let hasRequirementsExtracted = false; | |
// Generation de solutions | |
let solutionAccordionStates = {}; | |
let solutionsCriticizedVersions = []; | |
// checksum pour vérifier si les requirements séléctionnés ont changé | |
let lastSelectedRequirementsChecksum = null; | |
// ============================================================================= | |
// FONCTIONS MÉTIER | |
// ============================================================================= | |
/** | |
* Récupère la liste des meetings pour un working group | |
*/ | |
async function getMeetings() { | |
const workingGroup = document.getElementById('working-group-select').value; | |
if (!workingGroup) return; | |
showLoadingOverlay('Getting all available meetings...'); | |
toggleElementsEnabled(['get-meetings-btn'], false); | |
try { | |
const response = await fetch('/docs/get_meetings', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ working_group: workingGroup }) | |
}); | |
const data = await response.json(); | |
populateSelect('meeting-select', data.meetings, 'Select a meeting'); | |
toggleContainersVisibility(['meeting-container'], true); | |
} catch (error) { | |
console.error('Error while getting meetings:', error); | |
alert('Error while getting meetings.'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['get-meetings-btn'], true); | |
} | |
} | |
/** | |
* Récupère la liste des TDocs pour un meeting | |
*/ | |
async function getTDocs() { | |
const workingGroup = document.getElementById('working-group-select').value; | |
const meeting = document.getElementById('meeting-select').value; | |
if (!workingGroup || !meeting) return; | |
showLoadingOverlay('Getting TDocs List...'); | |
toggleElementsEnabled(['get-tdocs-btn'], false); | |
try { | |
const response = await fetch('/docs/get_dataframe', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ working_group: workingGroup, meeting: meeting }) | |
}); | |
const data = await response.json(); | |
populateDataTable(data.data); | |
setupFilters(data.data); | |
toggleContainersVisibility([ | |
'filters-container', | |
'action-buttons-container', | |
'doc-table-tab-contents', | |
], true); | |
switchTab('doc-table-tab'); | |
hasRequirementsExtracted = false; | |
} catch (error) { | |
console.error('Error while getting TDocs:', error); | |
alert('Error while getting TDocs'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['get-tdocs-btn'], true); | |
} | |
} | |
/** | |
* Remplit le tableau de données | |
* @param {Array} data - Données à afficher | |
*/ | |
function populateDataTable(data) { | |
const tbody = document.querySelector('#data-table tbody'); | |
tbody.innerHTML = ''; | |
data.forEach(row => { | |
const tr = document.createElement('tr'); | |
tr.setAttribute('data-type', row.Type || ''); | |
tr.setAttribute('data-status', row['TDoc Status'] || ''); | |
tr.setAttribute('data-agenda', row['Agenda item description'] || ''); | |
tr.innerHTML = ` | |
<td class="px-4 py-2"> | |
<input type="checkbox" class="row-checkbox"> | |
</td> | |
<td class="px-4 py-2" data-column="TDoc">${row.TDoc || ''}</td> | |
<td class="px-4 py-2" data-column="Title">${row.Title || ''}</td> | |
<td class="px-4 py-2" data-column="Type">${row.Type || ''}</td> | |
<td class="px-4 py-2" data-column="Status">${row['TDoc Status'] || ''}</td> | |
<td class="px-4 py-2" data-column="Agenda">${row['Agenda item description'] || ''}</td> | |
<td class="px-4 py-2" data-column="URL"> | |
<a href="${row.URL || '#'}" target="_blank" class="text-blue-500 hover:underline"> | |
${row.URL ? 'Lien' : 'N/A'} | |
</a> | |
</td> | |
`; | |
tbody.appendChild(tr); | |
}); | |
setupTableEvents(); | |
updateSelectedAndDisplayedCount(); | |
} | |
function setupFilters(data) { | |
// Extrait les valeurs uniques | |
const types = [...new Set(data.map(item => item.Type).filter(Boolean))]; | |
const statuses = [...new Set(data.map(item => item['TDoc Status']).filter(Boolean))]; | |
const agendaItems = [...new Set(data.map(item => item['Agenda item description']).filter(Boolean))]; | |
// Type (sélection unique DaisyUI) | |
populateDaisyDropdown('doc-type-filter-menu', types, 'doc-type-filter-label', type => { | |
selectedType = type; | |
applyFilters(); | |
}); | |
// Status (checkbox multiselect) | |
populateCheckboxDropdown('status-options', statuses, 'status', 'status-filter-label', selectedStatus, applyFilters); | |
// Agenda (checkbox multiselect) | |
populateCheckboxDropdown('agenda-options', agendaItems, 'agenda', 'agenda-filter-label', selectedAgenda, applyFilters); | |
// Initialisation des labels (optionnel) | |
document.getElementById('doc-type-filter-label').textContent = 'Type'; | |
document.getElementById('status-filter-label').textContent = 'Status (Tous)'; | |
document.getElementById('agenda-filter-label').textContent = 'Agenda Item (Tous)'; | |
} | |
/** | |
* Configure les événements des filtres | |
*/ | |
function setupFilterEvents() { | |
['doc-type-filter', 'doc-status-filter', 'agenda-item-filter'].forEach(filterId => { | |
document.getElementById(filterId).addEventListener('change', applyFilters); | |
}); | |
} | |
function updateSelectedAndDisplayedCount() { | |
// Lignes visibles (après filtrage) | |
const rows = document.querySelectorAll('#data-table tbody tr'); | |
let displayed = 0, selected = 0; | |
rows.forEach(row => { | |
// display: none signifie caché par le filtre | |
if (row.style.display === '' || row.style.display === undefined) { | |
displayed++; | |
const cb = row.querySelector('.row-checkbox'); | |
if (cb && cb.checked) selected++; | |
} | |
}); | |
document.getElementById('displayed-count').textContent = | |
`${displayed} total documents`; | |
document.getElementById('selected-count').textContent = | |
`${selected} selected documents`; | |
} | |
/** | |
* Applique les filtres au tableau | |
*/ | |
export function applyFilters() { | |
const rows = document.querySelectorAll('#data-table tbody tr'); | |
rows.forEach(row => { | |
const typeVal = row.getAttribute('data-type'); | |
const statusVal = row.getAttribute('data-status'); | |
const agendaVal = row.getAttribute('data-agenda'); | |
const typeMatch = !selectedType || typeVal === selectedType; | |
const statusMatch = !selectedStatus.size || selectedStatus.has(statusVal); | |
const agendaMatch = !selectedAgenda.size || selectedAgenda.has(agendaVal); | |
row.style.display = (typeMatch && statusMatch && agendaMatch) ? '' : 'none'; | |
}); | |
updateSelectedAndDisplayedCount?.(); | |
} | |
/** | |
* Configure les événements du tableau | |
*/ | |
function setupTableEvents() { | |
document.getElementById('select-all-checkbox').addEventListener('change', function () { | |
const checkboxes = document.querySelectorAll('.row-checkbox'); | |
checkboxes.forEach(checkbox => { | |
// Ne coche que les visibles | |
if (checkbox.closest('tr').style.display === '' || checkbox.closest('tr').style.display === undefined) { | |
checkbox.checked = this.checked; | |
} | |
}); | |
updateSelectedAndDisplayedCount(); | |
}); | |
// Listener sur chaque ligne | |
const rowCheckboxes = document.querySelectorAll('.row-checkbox'); | |
rowCheckboxes.forEach(cb => cb.addEventListener('change', updateSelectedAndDisplayedCount)); | |
// Compteur initial | |
updateSelectedAndDisplayedCount(); | |
} | |
/** | |
* Télécharge les TDocs sélectionnés | |
*/ | |
async function downloadTDocs() { | |
showLoadingOverlay('Downloading TDocs...'); | |
toggleElementsEnabled(['download-tdocs-btn', 'extract-requirements-btn'], false); | |
try { | |
// Extraire les données du tableau avec TDoc et URL | |
const selectedData = extractTableData({ 'TDoc': 'document', 'URL': 'url' }); | |
if (selectedData.length === 0) { | |
alert('Please select at least one document'); | |
return; | |
} | |
// Transformer au format requis: [{tdoc_id: url}, ...] | |
const documents = selectedData.map(obj => obj.document) | |
const response = await fetch('/docs/download_tdocs', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ documents: documents }) | |
}); | |
const blob = await response.blob(); | |
downloadBlob(blob, generateDownloadFilename()); | |
} catch (error) { | |
console.error(error); | |
alert('Error while downloading TDocs'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['download-tdocs-btn', 'extract-requirements-btn'], true); | |
} | |
} | |
/** | |
* Génère un nom de fichier pour le téléchargement | |
* @returns {string} Nom du fichier | |
*/ | |
function generateDownloadFilename() { | |
let filename = document.getElementById('meeting-select').value || 'documents'; | |
const agendaItems = selectedAgenda; | |
const docStatuses = selectedStatus | |
const docType = selectedType; | |
// empty set means "Tous" is selected | |
if (agendaItems) { | |
for (const aItem of agendaItems) { | |
filename += `_${aItem}`; | |
} | |
} | |
// empty set means "Tous" is selected | |
if (docStatuses) { | |
for (const docStatus of docStatuses) { | |
filename += `_${docStatus}`; | |
} | |
} | |
// empty means "Tous" | |
if (docType && docType !== "") { | |
filename = `${docType}_${filename}`; | |
} | |
if (hasRequirementsExtracted) { | |
filename = `requirements_${filename}`; | |
} | |
return `${filename}.zip`; | |
} | |
/** | |
* Télécharge un blob | |
* @param {Blob} blob - Blob à télécharger | |
* @param {string} filename - Nom du fichier | |
*/ | |
function downloadBlob(blob, filename) { | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = filename; | |
document.body.appendChild(a); | |
a.click(); | |
a.remove(); | |
window.URL.revokeObjectURL(url); | |
} | |
/** | |
* Extrait les requirements des documents sélectionnés | |
*/ | |
async function extractRequirements() { | |
const selectedData = extractTableData({ 'TDoc': 'document', 'URL': 'url' }); | |
if (selectedData.length === 0) { | |
alert('Please select at least one document'); | |
return; | |
} | |
showLoadingOverlay('Extracting requirements...'); | |
toggleElementsEnabled(['extract-requirements-btn'], false); | |
try { | |
const response = await postWithSSE('/docs/generate_requirements/sse', { documents: selectedData }, { | |
onMessage: (msg) => { | |
console.log("SSE message:"); | |
console.log(msg); | |
showLoadingOverlay(`Extracting requirements... (${msg.processed_docs}/${msg.total_docs})`); | |
}, | |
onError: (err) => { | |
console.error(`Error while fetching requirements: ${err}`); | |
throw err; | |
} | |
}); | |
const data = response.data; // data in the SSE message contains the requirements response | |
requirements = data.requirements; | |
let req_id = 0; | |
data.requirements.forEach(obj => { | |
obj.requirements.forEach(req => { | |
formattedRequirements.push({ | |
req_id, | |
"document": obj.document, | |
"context": obj.context, | |
"requirement": req | |
}) | |
req_id++; | |
}) | |
}) | |
displayRequirements(requirements); | |
// toggleContainersVisibility(['requirements-container', 'query-requirements-container'], true); | |
toggleContainersVisibility(['categorize-requirements-btn'], true); | |
// we got some requirements to the other tabs can be enabled | |
enableTabSwitching(); | |
// set the number of fetched requirements | |
document.getElementById('requirements-tab-badge').innerText = requirements.length; | |
hasRequirementsExtracted = true; | |
} catch (error) { | |
console.error('Error while extracting requirements', error); | |
alert('Error while extracting requirements'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['extract-requirements-btn'], true); | |
} | |
} | |
/** | |
* Affiche les requirements | |
* @param {Array} requirementsData - Données des requirements | |
*/ | |
function displayRequirements(requirementsData) { | |
const container = document.getElementById('requirements-list'); | |
container.innerHTML = ''; | |
requirementsData.forEach((docReq, docIndex) => { | |
const docDiv = document.createElement('div'); | |
docDiv.className = 'mb-6 p-4 border border-gray-200 rounded-lg bg-white'; | |
docDiv.innerHTML = ` | |
<h3 class="text-lg font-semibold mb-2">${docReq.document}</h3> | |
<p class="text-gray-600 mb-3">${docReq.context}</p> | |
<ul class="list-disc list-inside space-y-1"> | |
${docReq.requirements.map((req, reqIndex) => | |
`<li class="text-sm" data-req-id="${docIndex}-${reqIndex}">${req}</li>` | |
).join('')} | |
</ul> | |
`; | |
container.appendChild(docDiv); | |
}); | |
} | |
/** | |
* Catégorise les requirements | |
*/ | |
async function categorizeRequirements(max_categories) { | |
if (!formattedRequirements || formattedRequirements.length === 0) { | |
alert('No requirement available to categorize'); | |
return; | |
} | |
showLoadingOverlay('Categorizing requirements...'); | |
toggleElementsEnabled(['categorize-requirements-btn'], false); | |
try { | |
const response = await fetch('/requirements/categorize_requirements', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ requirements: formattedRequirements, "max_n_categories": max_categories }) | |
}); | |
const data = await response.json(); | |
categorizedRequirements = data; | |
displayCategorizedRequirements(categorizedRequirements.categories); | |
clearAllBootstrapSolutions(); | |
// Masquer le container de query et afficher les catégories et boutons solutions | |
// toggleContainersVisibility(['query-requirements-container'], false); | |
toggleContainersVisibility(['categorized-requirements-container', 'solutions-action-buttons-container'], true); | |
} catch (error) { | |
console.error('Error while categorizing requirements:', error); | |
alert('Error while categorizing requirements'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['categorize-requirements-btn'], true); | |
} | |
} | |
/** | |
* Affiche les requirements catégorisés | |
* @param {Array} categorizedData - Données des requirements catégorisés | |
*/ | |
function displayCategorizedRequirements(categorizedData) { | |
const container = document.getElementById('categorized-requirements-list'); | |
if (!container) { | |
console.error('Container element with ID "categorized-requirements-list" not found.'); | |
return; | |
} | |
container.innerHTML = ''; | |
categorizedData.forEach((category, categoryIndex) => { | |
const categoryDiv = document.createElement('div'); | |
categoryDiv.className = 'collapse collapse-arrow mb-2 border border-gray-200 rounded-lg bg-white shadow-sm'; | |
// Generate unique IDs for all checkboxes | |
const globalCheckboxId = `global-checkbox-${categoryIndex}`; | |
// Create the HTML for the individual requirement items within a category | |
const requirementsHTML = category.requirements.map((req, reqIndex) => { | |
const checkboxId = `checkbox-${categoryIndex}-${reqIndex}`; | |
return ` | |
<div class="p-2.5 bg-gray-50 rounded border-l-4 border-blue-400 flex items-start gap-3" data-category-index="${categoryIndex}" data-cat-req-id="${reqIndex}"> | |
<input type="checkbox" class="item-checkbox checkbox checkbox-sm mt-1" id="${checkboxId}" data-category-index="${categoryIndex}" /> | |
<label for="${checkboxId}" class="flex-1 cursor-pointer"> | |
<div class="text-sm font-medium text-gray-800">${req.document}</div> | |
<div class="text-sm text-gray-600">${req.requirement}</div> | |
</label> | |
</div>`; | |
}).join(''); | |
// Set the innerHTML for the entire collapsible category component | |
categoryDiv.innerHTML = ` | |
<!-- This hidden checkbox controls the collapse state --> | |
<input type="checkbox" name="collapse-accordion-${categoryIndex}" /> | |
<div class="collapse-title text-lg font-semibold text-blue-600"> | |
<!-- This wrapper prevents the collapse from triggering when clicking the checkbox or its label --> | |
<div class="checkbox-and-title-wrapper flex items-center gap-3"> | |
<input type="checkbox" class="global-checkbox checkbox" id="${globalCheckboxId}" data-category-index="${categoryIndex}" /> | |
<label for="${globalCheckboxId}" class="cursor-pointer">${category.title}</label> | |
</div> | |
</div> | |
<div class="collapse-content text-sm"> | |
<div class="space-y-2 p-2"> | |
${requirementsHTML} | |
</div> | |
</div> | |
`; | |
container.appendChild(categoryDiv); | |
}); | |
// --- Event Listeners --- | |
// Stop click propagation on the checkbox wrapper to isolate it from the collapse trigger | |
container.querySelectorAll('.checkbox-and-title-wrapper').forEach(wrapper => { | |
wrapper.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
}); | |
}); | |
// Handle "select all" logic when the global checkbox is changed | |
container.querySelectorAll('.global-checkbox').forEach(globalCheckbox => { | |
globalCheckbox.addEventListener('change', (e) => { | |
const categoryIndex = e.target.dataset.categoryIndex; | |
const itemCheckboxes = container.querySelectorAll(`.item-checkbox[data-category-index="${categoryIndex}"]`); | |
itemCheckboxes.forEach(checkbox => { | |
checkbox.checked = e.target.checked; | |
}); | |
}); | |
}); | |
// Update the global checkbox state when any individual item checkbox is changed | |
container.querySelectorAll('.item-checkbox').forEach(itemCheckbox => { | |
itemCheckbox.addEventListener('change', (e) => { | |
const categoryIndex = e.target.dataset.categoryIndex; | |
const itemCheckboxes = container.querySelectorAll(`.item-checkbox[data-category-index="${categoryIndex}"]`); | |
const globalCheckbox = container.querySelector(`.global-checkbox[data-category-index="${categoryIndex}"]`); | |
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked); | |
const someChecked = Array.from(itemCheckboxes).some(cb => cb.checked); | |
globalCheckbox.checked = allChecked; | |
// Set indeterminate state for better UX | |
if (allChecked) { | |
globalCheckbox.indeterminate = false; | |
} else if (someChecked) { | |
globalCheckbox.indeterminate = true; | |
} else { | |
globalCheckbox.indeterminate = false; | |
} | |
}); | |
}); | |
} | |
/* | |
* Copie la liste de requirements complète dans le presse papier | |
*/ | |
function copyAllRequirementsAsMarkdown() { | |
const formatted = requirements.map(doc => { | |
const header = `Document: ${doc.document}\nContext: ${doc.context}\nRequirements:\n`; | |
const reqs = doc.requirements.map((req, i) => ` ${i + 1}. ${req}`).join('\n'); | |
return `${header}${reqs}`; | |
}).join('\n\n'); | |
navigator.clipboard.writeText(formatted) | |
.then(() => { | |
console.log('Requirements copied to clipboard.'); | |
alert("Requirements copied to clipboard"); | |
}) | |
.catch(err => { | |
console.error('Failed to copy requirements:', err); | |
}); | |
} | |
/* | |
* Copie les requirements séléctionnés en markdown | |
*/ | |
function copySelectedRequirementsAsMarkdown() { | |
const selected = getSelectedRequirementsByCategory(); | |
if (!selected || !selected.categories || selected.categories.length === 0) { | |
alert("No selected requirements to copy."); | |
return; | |
} | |
const lines = []; | |
selected.categories.forEach(category => { | |
lines.push(`### ${category.title}`); | |
category.requirements.forEach(req => { | |
lines.push(`- ${req.requirement} (${req.document})`); | |
}); | |
lines.push(''); // Add an empty line after each category | |
}); | |
const markdownText = lines.join('\n'); | |
navigator.clipboard.writeText(markdownText).then(() => { | |
alert("Selected requirements copied to clipboard"); | |
}).catch(err => { | |
console.error("Failed to copy markdown:", err); | |
}); | |
} | |
/* | |
* Recupère tous les requirements séléctionnés par catégorie dans l'interface. | |
*/ | |
function getSelectedRequirementsByCategory() { | |
const container = document.getElementById('categorized-requirements-list'); | |
const selected_category_ids = []; | |
const categoryDivs = container.querySelectorAll('.collapse'); | |
categoryDivs.forEach((categoryDiv, categoryIndex) => { | |
// Find all checked item checkboxes within this category | |
const checkedItems = categoryDiv.querySelectorAll(`.item-checkbox[data-category-index="${categoryIndex}"]:checked`); | |
if (checkedItems.length > 0) { | |
// Extract requirement indexes from their parent div's data attribute | |
const checkedReqIndexes = Array.from(checkedItems).map(checkbox => { | |
const itemDiv = checkbox.closest('[data-cat-req-id]'); | |
return parseInt(itemDiv.dataset.catReqId, 10); | |
}); | |
selected_category_ids.push({ | |
categoryIndex, | |
checkedReqIndexes | |
}); | |
} | |
}); | |
/// Compute a checksum to check if checked requirements changed between two generations of solutions. | |
let totalChecksum = 0; | |
for (const { categoryIndex, checkedReqIndexes } of selected_category_ids) { | |
const catChecksum = checkedReqIndexes.reduce( | |
(sum, val, i) => sum + (val + 1) * (i + 1) ** 2, | |
0 | |
); | |
totalChecksum += (categoryIndex + 1) * catChecksum; // include category index for entropy | |
} | |
/// Reconstruct the schema based on the selected ids. | |
let selected_categories = { | |
categories: selected_category_ids.map(({ categoryIndex, checkedReqIndexes }) => { | |
const category = categorizedRequirements.categories[categoryIndex]; | |
const requirements = checkedReqIndexes.map(i => category.requirements[i]); | |
return { | |
id: categoryIndex, | |
title: category.title, | |
requirements, | |
}; | |
}), | |
requirements_checksum: totalChecksum, | |
}; | |
return selected_categories; | |
} | |
async function searchRequirements() { | |
const query = document.getElementById('query-input').value.trim(); | |
if (!query) { | |
alert('Please enter a search query'); | |
return; | |
} | |
if (!formattedRequirements || formattedRequirements.length === 0) { | |
alert('No available requirements for search'); | |
return; | |
} | |
showLoadingOverlay('Searching...'); | |
toggleElementsEnabled(['search-requirements-btn'], false); | |
try { | |
// Préparer les requirements pour la recherche | |
const response = await fetch('/requirements/get_reqs_from_query', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
query: query, | |
requirements: formattedRequirements | |
}) | |
}); | |
const data = await response.json(); | |
displaySearchResults(data.requirements); | |
} catch (error) { | |
console.error('Error while searching:', error); | |
alert('Error while searching requirements'); | |
} finally { | |
hideLoadingOverlay(); | |
toggleElementsEnabled(['search-requirements-btn'], true); | |
} | |
} | |
/** | |
* Affiche les résultats de recherche | |
* @param {Array} results - Résultats de la recherche | |
*/ | |
function displaySearchResults(results) { | |
const container = document.getElementById('query-results'); | |
container.innerHTML = ''; | |
if (results.length === 0) { | |
container.innerHTML = '<p class="text-gray-500">Aucun résultat trouvé pour cette requête.</p>'; | |
return; | |
} | |
const resultsDiv = document.createElement('div'); | |
resultsDiv.className = 'space-y-3'; | |
results.forEach((result, index) => { | |
const resultDiv = document.createElement('div'); | |
resultDiv.className = 'p-3 bg-blue-50 border border-blue-200 rounded-lg'; | |
resultDiv.innerHTML = ` | |
<div class="text-sm font-medium text-blue-800">${result.document}</div> | |
<div class="text-sm text-gray-600 mb-1">${result.context}</div> | |
<div class="text-sm">${result.requirement}</div> | |
`; | |
resultsDiv.appendChild(resultDiv); | |
}); | |
container.appendChild(resultsDiv); | |
} | |
// =========================================== Solution bootstrapping ============================================== | |
function createSolutionAccordion(solutionCriticizedHistory, containerId, versionIndex = 0, categoryIndex = null) { | |
const container = document.getElementById(containerId); | |
if (!container) { | |
console.error(`Container with ID "${containerId}" not found`); | |
return; | |
} | |
// Si categoryIndex est spécifié, ne mettre à jour que cette catégorie | |
if (categoryIndex !== null) { | |
updateSingleAccordion(solutionCriticizedHistory, containerId, versionIndex, categoryIndex); | |
return; | |
} | |
// Vider le container seulement si on recrée tout | |
container.innerHTML = ''; | |
// Récupérer les données de la version actuelle directement | |
const currentVersionData = solutionCriticizedHistory[versionIndex]; | |
// Créer l'accordéon principal | |
const accordion = document.createElement('div'); | |
accordion.className = 'space-y-2'; | |
accordion.id = 'main-accordion'; | |
// Afficher seulement les solutions de la version actuelle | |
currentVersionData.critiques.forEach((item, index) => { | |
createSingleAccordionItem(item, index, versionIndex, solutionCriticizedHistory, accordion); | |
}); | |
// Ajouter l'accordéon au container | |
container.appendChild(accordion); | |
} | |
function createSingleAccordionItem(item, index, versionIndex, solutionCriticizedHistory, accordion) { | |
const solution = item.solution; | |
const criticism = item.criticism; | |
// Récupérer le titre de la catégorie | |
const categoryTitle = categorizedRequirements.categories.find(c => c.id == solution['category_id']).title; | |
// Container pour chaque solution | |
const solutionCard = document.createElement('div'); | |
solutionCard.className = 'border border-gray-200 rounded-md shadow-sm solution-accordion'; | |
solutionCard.id = `accordion-item-${index}`; | |
// En-tête de l'accordéon avec navigation | |
const header = document.createElement('div'); | |
header.className = 'bg-gray-50 px-4 py-2 cursor-pointer hover:bg-gray-100 transition-colors duration-200'; | |
solutionCard.setAttribute('solution-accordion-id', `${index}`) | |
const currentVersion = versionIndex + 1; | |
const totalVersions = solutionCriticizedHistory.length; | |
header.innerHTML = ` | |
<div class="flex justify-between items-center"> | |
<div class="flex items-center space-x-3"> | |
<h3 class="text-sm font-semibold text-gray-800">${categoryTitle}</h3> | |
<div class="flex items-center space-x-2 bg-white px-3 py-1 rounded-full border"> | |
<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' : ''}" | |
data-solution-index="${solution['category_id']}" | |
${currentVersion === 1 ? 'disabled' : ''}> | |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path> | |
</svg> | |
</button> | |
<span class="text-xs font-medium text-gray-600 min-w-[60px] text-center version-indicator">Version ${currentVersion}</span> | |
<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' : ''}" | |
data-solution-index="${solution['category_id']}" | |
${currentVersion === totalVersions ? 'disabled' : ''}> | |
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<!-- | |
<button class="delete-btn text-red-500 hover:text-red-700 transition-colors" | |
data-solution-index="${solution['category_id']}" id="solution-delete-btn"> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<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" /> | |
</svg> | |
</button> | |
--> | |
</div>`; | |
// Contenu de l'accordéon | |
const content = document.createElement('div'); | |
content.className = `accordion-content px-4 py-3 space-y-3`; | |
content.id = `content-${solution['category_id']}`; | |
// enable solution drafting if private LLM info is present to enable solution private drafting | |
if (checkPrivateLLMInfoAvailable()) { | |
// Boutons pour passer la solution en draft | |
const body_btn_div = document.createElement('div'); | |
body_btn_div.className = "flex justify-end"; | |
const draft_btn = document.createElement('button'); | |
draft_btn.className = "btn btn-secondary rounded-full"; | |
draft_btn.innerText = "✏ Draft solution" | |
body_btn_div.appendChild(draft_btn); | |
content.appendChild(body_btn_div); | |
draft_btn.addEventListener('click', _ => { | |
// alert(`Drafting solution ${solution['category_id']} ${versionIndex}`) | |
moveSolutionToDrafts(solution); | |
}); | |
} | |
// Vérifier l'état d'ouverture précédent | |
const isOpen = solutionAccordionStates[solution['category_id']] || false; | |
console.log(isOpen); | |
if (!isOpen) | |
content.classList.add('hidden'); | |
// Section Critique | |
const critiqueSection = document.createElement('div'); | |
critiqueSection.className = 'bg-yellow-50 border-l-2 border-yellow-400 p-3 rounded-r-md'; | |
let critiqueContent = ` | |
<h4 class="text-sm font-semibold text-yellow-800 mb-2 flex items-center"> | |
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"> | |
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path> | |
</svg> | |
Critique & Analysis | |
</h4> | |
`; | |
// Sous-sections de critique | |
if (criticism.technical_challenges && criticism.technical_challenges.length > 0) { | |
critiqueContent += ` | |
<div class="mb-2"> | |
<h5 class="text-xs font-semibold text-yellow-700 mb-1">Technical Challenges:</h5> | |
<ul class="list-disc list-inside space-y-0.5 text-xs text-gray-700 ml-3"> | |
${criticism.technical_challenges.map(challenge => `<li>${challenge}</li>`).join('')} | |
</ul> | |
</div> | |
`; | |
} | |
if (criticism.weaknesses && criticism.weaknesses.length > 0) { | |
critiqueContent += ` | |
<div class="mb-2"> | |
<h5 class="text-xs font-semibold text-yellow-700 mb-1">Weaknesses:</h5> | |
<ul class="list-disc list-inside space-y-0.5 text-xs text-gray-700 ml-3"> | |
${criticism.weaknesses.map(weakness => `<li>${weakness}</li>`).join('')} | |
</ul> | |
</div> | |
`; | |
} | |
if (criticism.limitations && criticism.limitations.length > 0) { | |
critiqueContent += ` | |
<div class="mb-2"> | |
<h5 class="text-xs font-semibent text-yellow-700 mb-1">Limitations:</h5> | |
<ul class="list-disc list-inside space-y-0.5 text-xs text-gray-700 ml-3"> | |
${criticism.limitations.map(limitation => `<li>${limitation}</li>`).join('')} | |
</ul> | |
</div> | |
`; | |
} | |
critiqueSection.innerHTML = critiqueContent; | |
// ====================================================================================== | |
for (let item of buildSolutionSubCategories(solution)) | |
content.appendChild(item); | |
content.appendChild(critiqueSection); | |
// Événement de clic pour l'accordéon (exclure les boutons de navigation) | |
header.addEventListener('click', (e) => { | |
// Ne pas déclencher l'accordéon si on clique sur les boutons de navigation | |
if (e.target.closest('.version-btn-left') || e.target.closest('.version-btn-right') || e.target.closest('#solution-delete-btn')) { | |
return; | |
} | |
// handling open state | |
const isCurrentlyOpen = !content.classList.contains('hidden'); | |
solutionAccordionStates[solution['category_id']] = isCurrentlyOpen; | |
if (isCurrentlyOpen) | |
content.classList.add('hidden'); | |
else | |
content.classList.remove('hidden'); | |
}); | |
// Événements de navigation pour cette catégorie spécifique | |
header.querySelector('.version-btn-left')?.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
updateSingleAccordion(solutionCriticizedHistory, 'accordion-container', versionIndex - 1, index); | |
}); | |
header.querySelector('.version-btn-right')?.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
updateSingleAccordion(solutionCriticizedHistory, 'accordion-container', versionIndex + 1, index); | |
}); | |
// Assembler la carte de solution | |
solutionCard.appendChild(header); | |
solutionCard.appendChild(content); | |
accordion.appendChild(solutionCard); | |
} | |
function updateSingleAccordion(solutionCriticizedHistory, containerId, newVersionIndex, categoryIndex) { | |
// Vérifier les limites de version | |
if (newVersionIndex < 0 || newVersionIndex >= solutionCriticizedHistory.length) { | |
return; | |
} | |
const accordionItem = document.getElementById(`accordion-item-${categoryIndex}`); | |
if (!accordionItem) return; | |
const newData = solutionCriticizedHistory[newVersionIndex]; | |
const newItem = newData.critiques[categoryIndex]; | |
if (!newItem) return; | |
// Mettre à jour le contenu de cette catégorie spécifique | |
const tempContainer = document.createElement('div'); | |
createSingleAccordionItem(newItem, categoryIndex, newVersionIndex, solutionCriticizedHistory, tempContainer); | |
// Remplacer l'ancien item par le nouveau | |
accordionItem.parentNode.replaceChild(tempContainer.firstChild, accordionItem); | |
} | |
// Fonction d'initialisation simplifiée | |
function initializeSolutionAccordion(solutionCriticizedHistory, containerId, startVersion = 0) { | |
// Réinitialiser les états d'accordéon | |
solutionAccordionStates = {}; | |
createSolutionAccordion(solutionCriticizedHistory, containerId, startVersion); | |
document.getElementById(containerId).classList.remove('hidden') | |
} | |
// Supprime toutes les accordéons de solutions générées | |
//FIXME: À terme, ne devrait pas exister | |
function clearAllBootstrapSolutions() { | |
solutionAccordionStates = {} | |
solutionsCriticizedVersions = [] | |
document.querySelectorAll('.solution-accordion').forEach(a => a.remove()); | |
} | |
async function generateBootstrapSolutions(selected_categories, user_constraints = null) { | |
console.log(selected_categories); | |
let input_req = structuredClone(selected_categories); | |
input_req.user_constraints = user_constraints; | |
let response = await fetch("/solutions/bootstrap_solutions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) }) | |
let responseObj = await response.json() | |
return responseObj; | |
} | |
async function generateBootstrapCriticisms(solutions) { | |
let response = await fetch('/solutions/criticize_solution', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(solutions) }) | |
let responseObj = await response.json() | |
solutionsCriticizedVersions.push(responseObj) | |
} | |
async function refineBootstrapSolutions(critiques) { | |
let response = await fetch('/solutions/refine_solutions', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(critiques) }) | |
let responseObj = await response.json() | |
await generateBootstrapCriticisms(responseObj) | |
} | |
async function boostrapWorkflow(steps = 1) { | |
let soluce; | |
showLoadingOverlay('Boostrapping solutions ....'); | |
const selected_requirements = getSelectedRequirementsByCategory(); | |
const user_constraints = document.getElementById('additional-gen-instr').value; | |
console.log(user_constraints); | |
// check if the selected requirements changed since last workflow usage | |
const requirements_changed = selected_requirements.requirements_checksum != (lastSelectedRequirementsChecksum ?? -1); | |
for (let step = 1; step <= steps; step++) { | |
if (requirements_changed) { | |
clearAllBootstrapSolutions(); | |
console.log("Requirements checksum changed. Cleaning up"); | |
lastSelectedRequirementsChecksum = selected_requirements.requirements_checksum; | |
} | |
if (solutionsCriticizedVersions.length == 0) { | |
soluce = await generateBootstrapSolutions(selected_requirements, user_constraints ? user_constraints : null); | |
await generateBootstrapCriticisms(soluce) | |
} else { | |
let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1]; | |
await refineBootstrapSolutions(prevSoluce) | |
} | |
} | |
hideLoadingOverlay(); | |
initializeSolutionAccordion(solutionsCriticizedVersions, "solutions-list") | |
} | |
// ============================================================================= | |
// INITIALISATION DES ÉVÉNEMENTS | |
// ============================================================================= | |
document.addEventListener('DOMContentLoaded', function () { | |
// Bind tous les tabs | |
bindTabs(); | |
// Événements des boutons principaux | |
document.getElementById('working-group-select').addEventListener('change', (ev) => { | |
getMeetings(); | |
}); | |
document.getElementById('get-tdocs-btn').addEventListener('click', getTDocs); | |
document.getElementById('download-tdocs-btn').addEventListener('click', downloadTDocs); | |
document.getElementById('extract-requirements-btn').addEventListener('click', extractRequirements); | |
document.getElementById('categorize-requirements-btn').addEventListener('click', () => { | |
const category_count_auto_detect = document.getElementById('auto-detect-toggle').checked; | |
const n_categories = document.getElementById('category-count').value; | |
categorizeRequirements(category_count_auto_detect ? null : n_categories); | |
}); | |
// Événement pour la recherche | |
document.getElementById('search-requirements-btn').addEventListener('click', searchRequirements); | |
// Événements pour les boutons de solutions bootstrappées | |
document.getElementById('get-solutions-btn').addEventListener('click', () => { | |
const n_steps = document.getElementById('solution-gen-nsteps').value; | |
boostrapWorkflow(n_steps); | |
}); | |
document.getElementById('get-solutions-step-btn').addEventListener('click', () => { | |
boostrapWorkflow(1); | |
}); | |
// render l'ui de draft vide | |
renderDraftUI(); | |
handleLoadConfigFields(); | |
}); | |
// dseactiver le choix du nb de catégories lorsqu'en mode auto | |
document.getElementById('auto-detect-toggle').addEventListener('change', (ev) => { debounceAutoCategoryCount(ev.target.checked) }); | |
debounceAutoCategoryCount(true); | |
// focus l'input d'instructions de gen additionelles | |
document.getElementById("additional-gen-instr-btn").addEventListener('click', (ev) => { | |
document.getElementById('additional-gen-instr').focus() | |
}) | |
// copy requirements | |
document.getElementById('copy-reqs-btn').addEventListener('click', (ev) => { | |
copySelectedRequirementsAsMarkdown(); | |
}); | |
// copy all requirements | |
document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown); | |
// =============== settings events ================ | |
document.getElementById('settings-save-btn').addEventListener('click', handleSaveConfigFields); | |
document.getElementById('settings-clear-btn').addEventListener('click', handleClearConfig); | |
// button to fetch llm models | |
document.getElementById('settings-fetch-models').addEventListener('click', _ => { | |
const url = document.getElementById('settings-provider-url').value; | |
const token = document.getElementById('settings-provider-token').value; | |
populateLLMModelSelect('settings-provider-model', url, token).catch(e => alert("Error while fetching models: " + e)) | |
}); | |
// ================== solution drafting events ============= | |
// button to open full assessment modal | |
document.getElementById('read-assessment-button').addEventListener('click', _ => { | |
displayFullAssessment(); | |
}); | |
// Events des boutons pour le drafting de solutions | |
document.getElementById('refine-btn').addEventListener('click', handleDraftRefine); | |
document.getElementById('fto-analysis-btn').addEventListener('click', handleFTOAnalysis); |