| |
|
| | import { |
| | toggleElementsEnabled, toggleContainersVisibility, showLoadingOverlay, hideLoadingOverlay, populateSelect, |
| | populateCheckboxDropdown, updateCheckboxDropdownLabel, updateSelectedFilters, populateDaisyDropdown, updateFilterLabel, |
| | extractTableData, switchTab, enableTabSwitching, debounceAutoCategoryCount, |
| | bindTabs, checkCanUsePrivateGen |
| | } from "./ui-utils.js"; |
| | import { postWithSSE } from "./sse.js"; |
| |
|
| | |
| | let requirements = []; |
| |
|
| | |
| | let selectedType = ""; |
| | let selectedStatus = new Set(); |
| | let selectedAgenda = new Set(); |
| |
|
| | |
| | let accordionStates = {}; |
| | let formattedRequirements = []; |
| | let categorizedRequirements = []; |
| | let solutionsCriticizedVersions = []; |
| | |
| | let lastSelectedRequirementsChecksum = null; |
| | |
| | let hasRequirementsExtracted = false; |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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) { |
| | |
| | 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))]; |
| |
|
| | |
| | populateDaisyDropdown('doc-type-filter-menu', types, 'doc-type-filter-label', type => { |
| | selectedType = type; |
| | applyFilters(); |
| | }); |
| |
|
| | |
| | populateCheckboxDropdown('status-options', statuses, 'status', 'status-filter-label', selectedStatus, applyFilters); |
| |
|
| | |
| | populateCheckboxDropdown('agenda-options', agendaItems, 'agenda', 'agenda-filter-label', selectedAgenda, applyFilters); |
| |
|
| | |
| | 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)'; |
| | } |
| | |
| | |
| | |
| | function setupFilterEvents() { |
| | ['doc-type-filter', 'doc-status-filter', 'agenda-item-filter'].forEach(filterId => { |
| | document.getElementById(filterId).addEventListener('change', applyFilters); |
| | }); |
| | } |
| |
|
| | function updateSelectedAndDisplayedCount() { |
| | |
| | const rows = document.querySelectorAll('#data-table tbody tr'); |
| | let displayed = 0, selected = 0; |
| | rows.forEach(row => { |
| | |
| | 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`; |
| | } |
| |
|
| | |
| | |
| | |
| | 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?.(); |
| | } |
| |
|
| |
|
| | |
| | |
| | |
| | function setupTableEvents() { |
| | document.getElementById('select-all-checkbox').addEventListener('change', function () { |
| | const checkboxes = document.querySelectorAll('.row-checkbox'); |
| | checkboxes.forEach(checkbox => { |
| | |
| | if (checkbox.closest('tr').style.display === '' || checkbox.closest('tr').style.display === undefined) { |
| | checkbox.checked = this.checked; |
| | } |
| | }); |
| | updateSelectedAndDisplayedCount(); |
| | }); |
| |
|
| | |
| | const rowCheckboxes = document.querySelectorAll('.row-checkbox'); |
| | rowCheckboxes.forEach(cb => cb.addEventListener('change', updateSelectedAndDisplayedCount)); |
| |
|
| | |
| | updateSelectedAndDisplayedCount(); |
| | } |
| |
|
| | |
| | |
| | |
| | async function downloadTDocs() { |
| | showLoadingOverlay('Downloading TDocs...'); |
| | toggleElementsEnabled(['download-tdocs-btn', 'extract-requirements-btn'], false); |
| |
|
| | try { |
| | |
| | const selectedData = extractTableData({ 'TDoc': 'document', 'URL': 'url' }); |
| | if (selectedData.length === 0) { |
| | alert('Please select at least one document'); |
| | return; |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function generateDownloadFilename() { |
| | let filename = document.getElementById('meeting-select').value || 'documents'; |
| |
|
| | const agendaItems = selectedAgenda; |
| | const docStatuses = selectedStatus |
| | const docType = selectedType; |
| |
|
| | |
| | if (agendaItems) { |
| | for (const aItem of agendaItems) { |
| | filename += `_${aItem}`; |
| | } |
| | } |
| |
|
| | |
| | if (docStatuses) { |
| | for (const docStatus of docStatuses) { |
| | filename += `_${docStatus}`; |
| | } |
| | } |
| |
|
| | |
| | if (docType && docType !== "") { |
| | filename = `${docType}_${filename}`; |
| | } |
| |
|
| | if (hasRequirementsExtracted) { |
| | filename = `requirements_${filename}`; |
| | } |
| |
|
| | return `${filename}.zip`; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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); |
| | } |
| |
|
| | |
| | |
| | |
| | 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; |
| | 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(['categorize-requirements-btn'], true); |
| |
|
| | |
| | enableTabSwitching(); |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | 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); |
| | clearAllSolutions(); |
| |
|
| | |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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'; |
| |
|
| | |
| | const globalCheckboxId = `global-checkbox-${categoryIndex}`; |
| |
|
| | |
| | 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(''); |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| |
|
| | |
| | container.querySelectorAll('.checkbox-and-title-wrapper').forEach(wrapper => { |
| | wrapper.addEventListener('click', (e) => { |
| | e.stopPropagation(); |
| | }); |
| | }); |
| |
|
| | |
| | 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; |
| | }); |
| | }); |
| | }); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | if (allChecked) { |
| | globalCheckbox.indeterminate = false; |
| | } else if (someChecked) { |
| | globalCheckbox.indeterminate = true; |
| | } else { |
| | globalCheckbox.indeterminate = false; |
| | } |
| | }); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | 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); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | 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(''); |
| | }); |
| |
|
| | const markdownText = lines.join('\n'); |
| |
|
| | navigator.clipboard.writeText(markdownText).then(() => { |
| | console.log("Markdown copied to clipboard."); |
| | alert("Selected requirements copied to clipboard"); |
| | }).catch(err => { |
| | console.error("Failed to copy markdown:", err); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | function getSelectedRequirementsByCategory() { |
| | const container = document.getElementById('categorized-requirements-list'); |
| | const selected_category_ids = []; |
| |
|
| | const categoryDivs = container.querySelectorAll('.collapse'); |
| |
|
| | categoryDivs.forEach((categoryDiv, categoryIndex) => { |
| | |
| | const checkedItems = categoryDiv.querySelectorAll(`.item-checkbox[data-category-index="${categoryIndex}"]:checked`); |
| |
|
| | if (checkedItems.length > 0) { |
| | |
| | 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 |
| | }); |
| | } |
| | }); |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | 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 { |
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | 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); |
| | } |
| |
|
| | function createSolutionAccordion(solutionCriticizedHistory, containerId, versionIndex = 0, categoryIndex = null) { |
| | const container = document.getElementById(containerId); |
| | if (!container) { |
| | console.error(`Container with ID "${containerId}" not found`); |
| | return; |
| | } |
| |
|
| | |
| | if (categoryIndex !== null) { |
| | updateSingleAccordion(solutionCriticizedHistory, containerId, versionIndex, categoryIndex); |
| | return; |
| | } |
| |
|
| | |
| | container.innerHTML = ''; |
| |
|
| | |
| | const currentVersionData = solutionCriticizedHistory[versionIndex]; |
| |
|
| | |
| | const accordion = document.createElement('div'); |
| | accordion.className = 'space-y-2'; |
| | accordion.id = 'main-accordion'; |
| |
|
| | |
| | currentVersionData.critiques.forEach((item, index) => { |
| | createSingleAccordionItem(item, index, versionIndex, solutionCriticizedHistory, accordion); |
| | }); |
| |
|
| | |
| | container.appendChild(accordion); |
| | } |
| |
|
| | function createSingleAccordionItem(item, index, versionIndex, solutionCriticizedHistory, accordion) { |
| | const solution = item.solution; |
| | const criticism = item.criticism; |
| |
|
| | |
| | const categoryTitle = categorizedRequirements.categories.find(c => c.id == solution['Category_Id']).title; |
| |
|
| | |
| | const solutionCard = document.createElement('div'); |
| | solutionCard.className = 'border border-gray-200 rounded-md shadow-sm solution-accordion'; |
| | solutionCard.id = `accordion-item-${index}`; |
| |
|
| | |
| | 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>`; |
| |
|
| |
|
| | |
| | const content = document.createElement('div'); |
| | content.className = `accordion-content px-4 py-3 space-y-3`; |
| | content.id = `content-${solution['Category_Id']}`; |
| |
|
| | |
| | const isOpen = accordionStates[solution['Category_Id']] || false; |
| | console.log(isOpen); |
| | if (!isOpen) |
| | content.classList.add('hidden'); |
| |
|
| | |
| | const problemSection = document.createElement('div'); |
| | problemSection.className = 'bg-red-50 border-l-2 border-red-400 p-3 rounded-r-md'; |
| | problemSection.innerHTML = ` |
| | <h4 class="text-sm font-semibold text-red-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="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> |
| | </svg> |
| | Problem Description |
| | </h4> |
| | <p class="text-xs text-gray-700 leading-relaxed">${solution['Problem Description'] || 'Aucune description du problème disponible.'}</p> |
| | `; |
| |
|
| | |
| | const reqsSection = document.createElement('div'); |
| | reqsSection.className = "bg-gray-50 border-l-2 border-red-400 p-3 rounded-r-md"; |
| | const reqItemsUl = solution["Requirements"].map(req => `<li>${req}</li>`).join(''); |
| | reqsSection.innerHTML = ` |
| | <h4 class="text-sm font-semibold text-gray-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="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> |
| | </svg> |
| | Addressed requirements |
| | </h4> |
| | <ul class="list-disc pl-5 space-y-1 text-gray-700 text-xs"> |
| | ${reqItemsUl} |
| | </ul> |
| | ` |
| |
|
| | |
| | const solutionSection = document.createElement('div'); |
| | solutionSection.className = 'bg-green-50 border-l-2 border-green-400 p-3 rounded-r-md'; |
| | solutionSection.innerHTML = ` |
| | <h4 class="text-sm font-semibold text-green-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="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> |
| | </svg> |
| | Solution Description |
| | </h4> |
| | `; |
| |
|
| | |
| | const solContents = document.createElement('div'); |
| | solContents.className = "text-xs text-gray-700 leading-relaxed"; |
| | solutionSection.appendChild(solContents); |
| |
|
| | try { |
| | solContents.innerHTML = marked.parse(solution['Solution Description']); |
| | } |
| | catch (e) { |
| | solContents.innerHTML = `<p class="text-xs text-gray-700 leading-relaxed">${solution['Solution Description'] || 'No available solution description'}</p>`; |
| | } |
| |
|
| |
|
| | |
| | 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> |
| | `; |
| |
|
| | |
| | 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; |
| |
|
| | |
| |
|
| | const createEl = (tag, properties) => { |
| | const element = document.createElement(tag); |
| | Object.assign(element, properties); |
| | return element; |
| | }; |
| |
|
| | |
| | const sourcesSection = createEl('div', { |
| | className: 'bg-gray-50 border-l-2 border-gray-400 p-3 rounded-r-md' |
| | }); |
| |
|
| | const heading = createEl('h4', { |
| | className: 'text-sm font-semibold text-black mb-2 flex items-center', |
| | innerHTML: ` |
| | <svg |
| | xmlns="http://www.w3.org/2000/svg" |
| | class="w-4 h-4 mr-1" |
| | fill="none" |
| | viewBox="0 0 24 24" |
| | stroke="currentColor" |
| | stroke-width="2"> |
| | <path |
| | stroke-linecap="round" |
| | stroke-linejoin="round" |
| | 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" |
| | /> |
| | </svg> |
| | Sources |
| | ` |
| | }); |
| |
|
| | const pillContainer = createEl('div', { |
| | className: 'flex flex-wrap mt-1' |
| | }); |
| |
|
| | |
| | solution['References'].forEach(source => { |
| | const pillLink = createEl('a', { |
| | href: source.url, |
| | target: '_blank', |
| | rel: 'noopener noreferrer', |
| | 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', |
| | textContent: source.name |
| | }); |
| | pillContainer.appendChild(pillLink); |
| | }); |
| |
|
| | sourcesSection.append(heading, pillContainer); |
| |
|
| | |
| |
|
| | |
| | content.appendChild(problemSection); |
| | content.appendChild(reqsSection); |
| | content.appendChild(solutionSection); |
| | content.appendChild(critiqueSection); |
| | content.appendChild(sourcesSection); |
| |
|
| | |
| | header.addEventListener('click', (e) => { |
| | |
| | if (e.target.closest('.version-btn-left') || e.target.closest('.version-btn-right') || e.target.closest('#solution-delete-btn')) { |
| | return; |
| | } |
| |
|
| | |
| | const isCurrentlyOpen = !content.classList.contains('hidden'); |
| | accordionStates[solution['Category_Id']] = isCurrentlyOpen; |
| | if (isCurrentlyOpen) |
| | content.classList.add('hidden'); |
| | else |
| | content.classList.remove('hidden'); |
| | }); |
| |
|
| | |
| | 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); |
| | }); |
| |
|
| | |
| | solutionCard.appendChild(header); |
| | solutionCard.appendChild(content); |
| | accordion.appendChild(solutionCard); |
| | } |
| |
|
| | function updateSingleAccordion(solutionCriticizedHistory, containerId, newVersionIndex, categoryIndex) { |
| | |
| | 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; |
| |
|
| | |
| | const tempContainer = document.createElement('div'); |
| | createSingleAccordionItem(newItem, categoryIndex, newVersionIndex, solutionCriticizedHistory, tempContainer); |
| |
|
| | |
| | accordionItem.parentNode.replaceChild(tempContainer.firstChild, accordionItem); |
| | } |
| |
|
| | |
| | function initializeSolutionAccordion(solutionCriticizedHistory, containerId, startVersion = 0) { |
| | |
| | accordionStates = {}; |
| | createSolutionAccordion(solutionCriticizedHistory, containerId, startVersion); |
| | document.getElementById(containerId).classList.remove('hidden') |
| | } |
| |
|
| | |
| | |
| | function clearAllSolutions() { |
| | accordionStates = {} |
| | solutionsCriticizedVersions = [] |
| | document.querySelectorAll('.solution-accordion').forEach(a => a.remove()); |
| | } |
| |
|
| | async function generateSolutions(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/search_solutions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input_req) }) |
| | let responseObj = await response.json() |
| | return responseObj; |
| | } |
| |
|
| | async function generateCriticisms(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 refineSolutions(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 generateCriticisms(responseObj) |
| | } |
| |
|
| | async function workflow(steps = 1) { |
| | let soluce; |
| | showLoadingOverlay('Génération des solutions & critiques ....'); |
| |
|
| | const selected_requirements = getSelectedRequirementsByCategory(); |
| | const user_constraints = document.getElementById('additional-gen-instr').value; |
| |
|
| | console.log(user_constraints); |
| |
|
| | |
| | const requirements_changed = selected_requirements.requirements_checksum != (lastSelectedRequirementsChecksum ?? -1); |
| |
|
| | for (let step = 1; step <= steps; step++) { |
| | if (requirements_changed) { |
| | clearAllSolutions(); |
| | console.log("Requirements checksum changed. Cleaning up"); |
| | lastSelectedRequirementsChecksum = selected_requirements.requirements_checksum; |
| | } |
| |
|
| | if (solutionsCriticizedVersions.length == 0) { |
| | soluce = await generateSolutions(selected_requirements, user_constraints ? user_constraints : null); |
| | await generateCriticisms(soluce) |
| | } else { |
| | let prevSoluce = solutionsCriticizedVersions[solutionsCriticizedVersions.length - 1]; |
| | await refineSolutions(prevSoluce) |
| | } |
| | } |
| | hideLoadingOverlay(); |
| | initializeSolutionAccordion(solutionsCriticizedVersions, "solutions-list") |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | document.addEventListener('DOMContentLoaded', function () { |
| | bindTabs(); |
| | |
| | |
| | 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); |
| | }); |
| |
|
| | |
| | document.getElementById('search-requirements-btn').addEventListener('click', searchRequirements); |
| |
|
| | |
| | document.getElementById('get-solutions-btn').addEventListener('click', () => { |
| | const n_steps = document.getElementById('solution-gen-nsteps').value; |
| | workflow(n_steps); |
| | }); |
| | document.getElementById('get-solutions-step-btn').addEventListener('click', () => { |
| | workflow(1); |
| | }); |
| | }); |
| |
|
| | |
| | document.getElementById('auto-detect-toggle').addEventListener('change', (ev) => { debounceAutoCategoryCount(ev.target.checked) }); |
| | debounceAutoCategoryCount(true); |
| |
|
| | |
| | document.getElementById("additional-gen-instr-btn").addEventListener('click', (ev) => { |
| | document.getElementById('additional-gen-instr').focus() |
| | }) |
| |
|
| | document.getElementById('copy-reqs-btn').addEventListener('click', (ev) => { |
| | copySelectedRequirementsAsMarkdown(); |
| | }); |
| |
|
| | document.getElementById('copy-all-reqs-btn').addEventListener('click', copyAllRequirementsAsMarkdown); |
| |
|
| | document.getElementById('test-btn').addEventListener('click', _ => { |
| | console.log(checkCanUsePrivateGen()); |
| | }); |