import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/16.1.1/lib/marked.esm.js'; import { assessSolution, getModelList, refineSolution } from "./gen.js" // ============================================================================= // FONCTIONS UTILITAIRES POUR LA GESTION DES ÉLÉMENTS // ============================================================================= /** * Active/désactive des éléments par leurs IDs * @param {string[]} elementIds - Liste des IDs des éléments à activer * @param {boolean} enabled - true pour activer, false pour désactiver */ export function toggleElementsEnabled(elementIds, enabled = true) { elementIds.forEach(id => { const element = document.getElementById(id); if (element) { if (enabled) { element.removeAttribute('disabled'); } else { element.setAttribute('disabled', 'true'); } } }); } /** * Affiche/masque des conteneurs par leurs IDs * @param {string[]} containerIds - Liste des IDs des conteneurs à afficher * @param {boolean} visible - true pour afficher, false pour masquer */ export function toggleContainersVisibility(containerIds, visible = true) { containerIds.forEach(id => { const container = document.getElementById(id); if (container) { if (visible) { container.classList.remove('hidden'); } else { container.classList.add('hidden'); } } }); } /** * Affiche le loading overlay avec un message personnalisé * @param {string} message - Message à afficher */ export function showLoadingOverlay(message = 'Chargement en cours...') { document.getElementById('progress-text').textContent = message; toggleContainersVisibility(['loading-overlay'], true); } /** * Masque le loading overlay */ export function hideLoadingOverlay() { toggleContainersVisibility(['loading-overlay'], false); } /** * Réinitialise un select et ajoute des options * @param {string} selectId - ID du select * @param {Object} options - Objet avec les options {value: text} * @param {string} defaultText - Texte par défaut */ export function populateSelect(selectId, options, defaultText = 'Sélectionner...') { const select = document.getElementById(selectId); if (select) { select.innerHTML = ``; Object.entries(options).forEach(([text, value]) => { const option = document.createElement('option'); option.value = value; option.textContent = text; select.appendChild(option); }); } } export function populateCheckboxDropdown(optionsContainerId, options, filterType, labelId, selectionSet, onSelect) { const container = document.getElementById(optionsContainerId); container.innerHTML = ''; selectionSet.clear(); // reset all // Ajoute chaque option options.forEach(option => { const safeId = `${filterType}-${encodeURIComponent(option).replace(/[%\s]/g, '_')}`; const label = document.createElement('label'); label.className = "flex items-center gap-2 cursor-pointer py-1"; label.innerHTML = ` ${option} `; label.querySelector('input').addEventListener('change', function () { if (this.checked) { selectionSet.add(this.value); } else { selectionSet.delete(this.value); } // Gestion du label "Tous" updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length); // Gestion du "Tous" global const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`); if (allBox && allBox.checked) allBox.checked = false; // Si plus rien n'est coché, recoche "Tous" if (selectionSet.size === 0 && allBox) allBox.checked = true; onSelect?.(); }); container.appendChild(label); }); // Réinitialise le label updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length); // Gestion de "Tous" const allBox = document.querySelector(`.${filterType}-checkbox[value="all"]`); if (allBox) { allBox.addEventListener('change', function () { if (this.checked) { // Décoche tout le reste selectionSet.clear(); container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false); this.checked = true; // reste coché updateCheckboxDropdownLabel(filterType, labelId, selectionSet, options.length); applyFilters(); } }); } } export function updateCheckboxDropdownLabel(type, labelId, set, totalCount) { const label = document.getElementById(labelId); if (!set.size) { label.textContent = type.charAt(0).toUpperCase() + type.slice(1) + " (Tous)"; } else if (set.size === 1) { label.textContent = [...set][0]; } else { label.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)} (${set.size}/${totalCount})`; } } export function updateSelectedFilters(filterType, value, isChecked) { if (isChecked) { selectedFilters[filterType].add(value); } else { selectedFilters[filterType].delete(value); } } export function populateDaisyDropdown(menuId, options, labelId, onSelect) { const menu = document.getElementById(menuId); menu.innerHTML = ''; // Option "Tous" const liAll = document.createElement('li'); liAll.innerHTML = `Tous`; liAll.querySelector('a').onclick = e => { e.preventDefault(); document.getElementById(labelId).textContent = "Type"; onSelect(""); }; menu.appendChild(liAll); // Ajoute chaque option options.forEach(opt => { const li = document.createElement('li'); li.innerHTML = `${opt}`; li.querySelector('a').onclick = e => { e.preventDefault(); document.getElementById(labelId).textContent = opt; onSelect(opt); }; menu.appendChild(li); }); } export function updateFilterLabel(filterType) { const selectedCount = selectedFilters[filterType].size; const labelElement = document.getElementById(`${filterType}-filter-label`); if (selectedCount === 0) { labelElement.textContent = `${filterType} (Tous)`; } else { labelElement.textContent = `${filterType} (${selectedCount} sélectionné${selectedCount > 1 ? 's' : ''})`; } } /** * Extrait les données du tableau selon un mapping * @param {Object} mapping - Mapping des colonnes {columnName: propertyName} * @returns {Array} Données extraites */ export function extractTableData(mapping) { const tbody = document.querySelector('#data-table tbody'); const rows = tbody.querySelectorAll('tr'); const data = []; rows.forEach(row => { const checkboxes = row.querySelectorAll('input[type="checkbox"]:checked'); if (checkboxes.length > 0) { const rowData = {}; Object.entries(mapping).forEach(([columnName, propertyName]) => { const cell = row.querySelector(`td[data-column="${columnName}"]`); if (cell) { if (columnName == "URL") { rowData[propertyName] = cell.querySelector('a').getAttribute('href'); } else { rowData[propertyName] = cell.textContent.trim(); } } }); data.push(rowData); } }); return data; } /** * Construit les sous-catégories communes dans l'affichage des solutions */ export function buildSolutionSubCategories(solution) { // Section Problem Description const problemSection = document.createElement('div'); problemSection.className = 'bg-red-50 border-l-2 border-red-400 p-3 rounded-r-md'; problemSection.innerHTML = `

Problem Description

${solution["problem_description"] || 'Aucune description du problème disponible.'}

`; // Section Problem requirements 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 => `
  • ${req}
  • `).join(''); reqsSection.innerHTML = `

    Addressed 3GPP requirements

    ` // Section Solution Description const solutionSection = document.createElement('div'); solutionSection.className = 'bg-green-50 border-l-2 border-green-400 p-3 rounded-r-md'; solutionSection.innerHTML = `

    Solution Description

    `; // container for markdown content 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) { console.error(e); solContents.innerHTML = `

    ${solution['solution_description'] || 'No available solution description'}

    `; } return [problemSection, reqsSection, solutionSection] } const TABS = { 'doc-table-tab': 'doc-table-tab-contents', 'requirements-tab': 'requirements-tab-contents', 'solutions-tab': 'solutions-tab-contents', 'query-tab': 'query-tab-contents', 'draft-tab': 'draft-tab-contents' }; /** * Bascule l'affichage sur le nouveau tab * @param {*} newTab */ export function switchTab(newTab) { // Remove active tab style from all tabs Object.keys(TABS).forEach(tabId => { const tabElement = document.getElementById(tabId); if (tabElement) { tabElement.classList.remove("tab-active"); } }); // Hide all tab contents Object.values(TABS).forEach(contentId => { const contentElement = document.getElementById(contentId); if (contentElement) { contentElement.classList.add("hidden"); } }); // Activate the new tab if it exists in the mapping if (newTab in TABS) { const newTabElement = document.getElementById(newTab); const newContentElement = document.getElementById(TABS[newTab]); if (newTabElement) newTabElement.classList.add("tab-active"); if (newContentElement) newContentElement.classList.remove("hidden"); } } /** * Setup les boutons pour basculer vers un autre tab */ export function bindTabs() { Object.keys(TABS).forEach(tabId => { const tabElement = document.getElementById(tabId); tabElement.addEventListener('click', _ => switchTab(tabId)); }); } /** * Bascule l'affichage vers la tab uniquement si les requirements sont */ export function enableTabSwitching() { Object.keys(TABS).forEach(tabId => { const tab = document.getElementById(tabId); if (tab) tab.classList.remove("tab-disabled"); }) } /** * Change l'état d'activation du number box de choix de nb de catégories. */ export function debounceAutoCategoryCount(state) { document.getElementById('category-count').disabled = state; } export function getPrivateLLMInfo() { const provider_url = document.getElementById('settings-provider-url').value; const provider_token = document.getElementById('settings-provider-token').value; const provider_model = document.getElementById('settings-provider-model').value; const assessment_rules = document.getElementById('settings-assessment-rules').value; const portfolio_info = document.getElementById('settings-portfolio').value; return { provider_url, provider_token, provider_model, assessment_rules, portfolio_info }; } /** * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée. */ export function checkPrivateLLMInfoAvailable() { const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo(); const isEmpty = (str) => (!str?.length); return !isEmpty(provider_url) && !isEmpty(provider_token) && !isEmpty(assessment_rules) && !isEmpty(portfolio_info) && !isEmpty(provider_model); // return true; } /** * Populates a select element with model names fetched from the API. * @param {string} selectElementId The ID of the HTML select element to populate. * @param {string} providerUrl The API provider URL. * @param {string} apiKey The API key. */ export async function populateLLMModelSelect(selectElementId, providerUrl, apiKey) { const selectElement = document.getElementById(selectElementId); if (!selectElement) { console.error(`Select element with ID "${selectElementId}" not found.`); return; } // Clear the "Loading..." option or any existing options selectElement.innerHTML = ''; try { const models = await getModelList(providerUrl, apiKey); if (models.length === 0) { const option = document.createElement('option'); option.value = ""; option.textContent = "No models found"; selectElement.appendChild(option); selectElement.disabled = true; // Disable if no models return; } // Add a default "Please select" option const defaultOption = document.createElement('option'); defaultOption.value = ""; // Or a placeholder like "select-model" defaultOption.textContent = "Select a model"; defaultOption.disabled = true; // Make it unselectable initially defaultOption.selected = true; // Make it the default selected option selectElement.appendChild(defaultOption); // Populate with the fetched models models.forEach(modelName => { const option = document.createElement('option'); option.value = modelName; option.textContent = modelName; selectElement.appendChild(option); }); } catch (error) { throw error; } } // ================================================================================ Solution drafting using private LLMs ========================================================== /** History of previously created drafts * The draftHistory will look like this: * { * solution: {} - the solution object * insights: [ * { id: 'i1', text: 'Some insight text', checked: false }, * { id: 'i2', text: 'Another insight', checked: true } * ], * assessment_full: The full assessment text * } */ let draftHistory = []; // Index of the latest draft in the draft history. // -1 means theres no draft. let draftCurrentIndex = -1; /** * Passe une solution bootstrappée en draft pour être itérée sur le private compute * @param {Object} solution - Un objet qui représente une solution bootstrappée (SolutionModel). */ export function moveSolutionToDrafts(solution) { const draft_tab_item = document.getElementById('draft-tab'); if (draft_tab_item.classList.contains("hidden")) // un-hide the draft tab the first time a solution is drafted draft_tab_item.classList.remove("hidden"); switchTab('draft-tab'); const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo(); showLoadingOverlay("Assessing solution ...."); assessSolution(provider_url, provider_model, provider_token, solution, assessment_rules, portfolio_info).then(response => { // reset the state of the draft history draftHistory = []; draftCurrentIndex = -1; const insights = response.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false })); // push the solution to the draft history draftHistory.push({ solution: solution, insights: insights, assessment_full: response.assessment_full, final_verdict: response.extracted_info.final_verdict, assessment_summary: response.extracted_info.summary, }); draftCurrentIndex++; // update the UI by rendering it renderDraftUI(); }).catch(e => { alert(e); }).finally(() => { hideLoadingOverlay(); }) } /** * SIMULATED API CALL * In a real app, this would be an async fetch call to a backend AI/service. * @param {string} previousSolution - The text of the previous solution. * @param {Array} selectedInsights - An array of the selected insight texts. * @returns {object} - An object with the new solution and new insights. */ function assessSolutionDraft(previousSolution, selectedInsights = []) { const version = draftHistory.length + 1; let newSolutionText; if (selectedInsights.length > 0) { newSolutionText = `V${version}: Based on "${previousSolution.substring(0, 30)}..." and the insights: [${selectedInsights.join(', ')}], we've refined the plan to focus more on digital outreach and local partnerships.`; } else { newSolutionText = `V${version}: This is an initial assessment of "${previousSolution.substring(0, 50)}...". The plan seems solid but could use more detail.`; } // Generate some random new insights const newInsights = [ { id: `v${version}-i1`, text: `Consider social media marketing (Insight ${Math.floor(Math.random() * 100)})`, checked: false }, { id: `v${version}-i2`, text: `Focus on customer retention (Insight ${Math.floor(Math.random() * 100)})`, checked: false }, { id: `v${version}-i3`, text: `Expand the product line (Insight ${Math.floor(Math.random() * 100)})`, checked: false }, ]; return { newSolutionText, newInsights }; } /** * Renders the timeline UI based on the current state * @param {Number} currentIndex - Current index for latest draft * @param {Array} drafts - Current history of previous drafts */ function renderDraftTimeline(timelineContainer, currentIndex, drafts) { timelineContainer.innerHTML = ''; drafts.forEach((state, idx) => { const li = document.createElement('li'); li.className = `step ${idx <= currentIndex ? 'step-primary' : ''}`; li.textContent = `Draft #${idx + 1}`; li.onclick = () => jumpToDraft(idx); timelineContainer.appendChild(li); }); } /** * Renders the entire UI based on the current state (draftHistory[currentIndex]). */ export function renderDraftUI() { const solutionDisplay = document.getElementById('solution-draft-display'); const insightsContainer = document.getElementById('insights-container'); const timelineContainer = document.getElementById('timeline-container'); if (draftCurrentIndex < 0) { solutionDisplay.innerHTML = `

    No drafted solutions for now

    ` insightsContainer.innerHTML = ''; timelineContainer.innerHTML = ''; return; } const currentState = draftHistory[draftCurrentIndex]; const solutionSections = buildSolutionSubCategories(currentState.solution); solutionDisplay.innerHTML = ''; // 1. Render Solution for (let child of solutionSections) solutionDisplay.appendChild(child); //3. but 2. actually: Print verdict and quick summary document.getElementById('assessment-recommendation-status').innerText = currentState.final_verdict; document.getElementById('assessment-recommendation-summary').innerText = currentState.assessment_summary; // 2. Render Insights Checkboxes insightsContainer.innerHTML = ''; currentState.insights.forEach(insight => { const isChecked = insight.checked ? 'checked' : ''; const insightEl = document.createElement('label'); insightEl.className = 'label cursor-pointer justify-start gap-4'; insightEl.innerHTML = ` ${insight.text} `; // Add event listener to update state on check/uncheck insightEl.querySelector('input').addEventListener('change', (e) => { insight.checked = e.target.checked; }); insightsContainer.appendChild(insightEl); }); // Render the timeline with the fetched timeline container renderDraftTimeline(timelineContainer, draftCurrentIndex, draftHistory); console.log(draftHistory); console.log(draftCurrentIndex); } /** * Handles the "Refine" button click. */ export function handleDraftRefine() { // Fetch DOM elements here const refineBtn = document.getElementById('refine-btn'); const currentState = draftHistory[draftCurrentIndex]; // Get selected insights from the current state const selectedInsights = currentState.insights .filter(i => i.checked) .map(i => i.text); if (selectedInsights.length === 0) { alert('Please select at least one insight to refine the solution.'); return; } // --- THIS IS THE KEY LOGIC FOR INVALIDATING THE FUTURE --- // If we are not at the end of the timeline, chop off the future states. if (draftCurrentIndex < draftHistory.length - 1) { draftHistory = draftHistory.slice(0, draftCurrentIndex + 1); } // --- const { provider_url, provider_token, provider_model, assessment_rules, portfolio_info } = getPrivateLLMInfo(); showLoadingOverlay('Refining and assessing ....') refineSolution(provider_url, provider_model, provider_token, currentState.solution, selectedInsights, assessment_rules, portfolio_info) .then(newSolution => { const refinedSolution = newSolution; return assessSolution(provider_url, provider_model, provider_token, newSolution, assessment_rules, portfolio_info) .then(assessedResult => { return { refinedSolution, assessedResult }; }); }) .then(result => { const newInsights = result.assessedResult.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false })); draftHistory.push({ solution: result.refinedSolution, insights: newInsights, assessment_full: result.assessedResult.assessment_full, final_verdict: result.assessedResult.extracted_info.final_verdict, assessment_summary: result.assessedResult.extracted_info.summary, }); draftCurrentIndex++; renderDraftUI(); }) .catch(error => { // Handle any errors alert("An error occurred:" + error); }).finally(() => { hideLoadingOverlay(); }); } /** * Jumps to a specific state in the draftHistory timeline. */ function jumpToDraft(index) { if (index >= 0 && index < draftHistory.length) { draftCurrentIndex = index; renderDraftUI(); } } export function displayFullAssessment() { const full_assessment_content = document.getElementById('read-assessment-content'); const modal = document.getElementById('read-assessment-modal'); if (draftCurrentIndex < 0) return; const lastDraft = draftHistory[draftCurrentIndex]; try { full_assessment_content.innerHTML = marked.parse(lastDraft.assessment_full); } catch (e) { full_assessment_content.innerHTML = lastDraft.assessment_full; } modal.showModal(); }