import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/16.1.1/lib/marked.esm.js'; // import { JSZip } from 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/+esm'; import { assessSolution, getModelList, refineSolution, runFTOAnalysis } from "./gen.js" import { clearConfig, loadConfig, saveConfig } from "./persistence.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 = `
${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 => `${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; } // ============================================================================================ Overlay des paramètres ==================================================================== /** * Récupère les valeurs des champs de config des infos LLM. * @returns */ export function getConfigFields() { const providerUrl = document.getElementById('settings-provider-url').value; const providerToken = document.getElementById('settings-provider-token').value; const providerModel = document.getElementById('settings-provider-model').value; const assessmentRules = document.getElementById('settings-assessment-rules').value; const businessPortfolio = document.getElementById('settings-portfolio').value; const ftoTopicCount = document.getElementById('settings-fto-topic-count').value; return { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio, ftoTopicCount }; } /** * Vérifie si les paramètres sont bien renseignés pour utiliser la génération privée. */ export function checkPrivateLLMInfoAvailable() { const { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields(); const isEmpty = (str) => (!str?.length); return !isEmpty(providerUrl) && !isEmpty(providerToken) && !isEmpty(assessmentRules) && !isEmpty(businessPortfolio) && !isEmpty(providerModel); // 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; } } /** * Charge le contenu de la configuration locale dans les champs HTML. */ export function handleLoadConfigFields() { const configuration = loadConfig(); if (configuration === null) return; const providerUrl = document.getElementById('settings-provider-url'); const providerToken = document.getElementById('settings-provider-token'); const providerModel = document.getElementById('settings-provider-model'); const assessmentRules = document.getElementById('settings-assessment-rules'); const businessPortfolio = document.getElementById('settings-portfolio'); const ftoTopicCount = document.getElementById('settings-fto-topic-count'); providerUrl.value = configuration.providerUrl; providerToken.value = configuration.providerToken; assessmentRules.value = configuration.assessmentRules; businessPortfolio.value = configuration.businessPortfolio; ftoTopicCount.value = configuration.ftoTopicCount; // on doit d'abord recup les modeles avant de set la valeur populateLLMModelSelect('settings-provider-model', configuration.providerUrl, configuration.providerToken).then(() => { providerModel.value = configuration.providerModel; }).catch(e => { alert("Failed to set LLM model in model selector. Model may not be available anymore, check model in LLM settings."); }) } /** * Sauvegarde le contenu des champs dans la configuration locale. */ export function handleSaveConfigFields() { saveConfig(getConfigFields()); alert("Configuration saved locally."); } /** * Clear le contenu de la configuration stockée dans localStorage. * LA CONFIGURATION DANS LES CHAMPS HTML N'EST PAS AFFECTEE. */ export function handleClearConfig() { clearConfig(); alert("Saved configuration has been cleared. Configuration set in the fields won't be saved."); } // ================================================================================ 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 { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields(); showLoadingOverlay("Assessing solution ...."); assessSolution(providerUrl, providerModel, providerToken, solution, assessmentRules, businessPortfolio).then(response => { // reset the state of the draft history draftHistory = []; draftCurrentIndex = -1; // map from a list of insights to a selectable list of insights const insights = response.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false })); // push the solution to the draft history draftHistory.push({ type: "draft", 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(); }) } /** * 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.innerHTML = ` ${state.type == "draft" ? `Draft #${idx + 1}` : "FTO analysis "}` // li.textContent = ; // li.setAttribute('data-content', state.type == "draft" ? "D" : "F") 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 the different solution sections for (let child of solutionSections) solutionDisplay.appendChild(child); // 2. render final verdict and the quick summary const finalVerdictTextEl = document.getElementById('assessment-recommendation-status'); // maps final verdict to text color const verdict_colors = { NO_GO: "text-red-600", CONDITIONAL_GO: "text-orange-600", IMMEDIATE_GO: "text-green-600" }; // reset color of the text Object.values(verdict_colors).forEach(v => { finalVerdictTextEl.classList.remove(v); }); finalVerdictTextEl.innerText = currentState.final_verdict; finalVerdictTextEl.classList.add(verdict_colors[currentState.final_verdict.replace("-", "_")]); 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 userInsightsText = document.getElementById('user-insight-text').value; const currentState = draftHistory[draftCurrentIndex]; // Get selected insights text from the current state const selectedInsights = currentState.insights .filter(i => i.checked) .map(i => i.text); if (selectedInsights.length === 0 && (userInsightsText === null || userInsightsText === "")) { alert('Please select at least one insight to refine the solution or provide a manual user insight.'); return; } // 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 { providerUrl, providerToken, providerModel, assessmentRules, businessPortfolio } = getConfigFields(); showLoadingOverlay('Refining and assessing ....') refineSolution(providerUrl, providerModel, providerToken, currentState.solution, selectedInsights, userInsightsText, assessmentRules, businessPortfolio) .then(newSolution => { const refinedSolution = newSolution; return assessSolution(providerUrl, providerModel, providerToken, newSolution, assessmentRules, businessPortfolio) .then(assessedResult => { return { refinedSolution, assessedResult }; }); }) .then(result => { // map from a list of insights to a selectable list of insights const newInsights = result.assessedResult.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false })); draftHistory.push({ type: "draft", 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 while refining a draft:" + 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 handleFTOAnalysis() { const { providerUrl, providerToken, providerModel, businessPortfolio, ftoTopicCount } = getConfigFields(); const currentState = draftHistory[draftCurrentIndex]; console.log("Launching FTO analysis"); showLoadingOverlay("Running FTO analysis... This may take a while"); runFTOAnalysis(providerUrl, providerModel, providerToken, currentState.solution, businessPortfolio, ftoTopicCount) .then(result => { // map from a list of insights to a selectable list of insights const newInsights = result.extracted_info.insights.map((e, idx, __) => ({ id: idx, text: e, checked: false })); // maps the original fto report assessment + the actual contents for displaying const full_assessment_content = `${result.assessment_full}\n\n---\n---\n\n# FTO report contents\n\n${result.fto_report}`; draftHistory.push({ type: "fto", solution: currentState.solution, insights: newInsights, assessment_full: full_assessment_content, final_verdict: result.extracted_info.final_verdict, assessment_summary: result.extracted_info.summary, }); draftCurrentIndex++; renderDraftUI(); }) .catch(e => alert(e)) .finally(() => hideLoadingOverlay()); } /** * Displays the whole idea evaluation. */ 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(); } /** * Exports asynchronously all drafts in the timeline */ export async function handleExportDrafts() { if (draftHistory.length === 0) { alert("No drafts to export!"); return; } console.log("Starting ZIP export..."); const zip = new JSZip(); const separator = '-----------------------------------------'; // Loop through each draft in the history draftHistory.forEach((draft, index) => { const fileContent = `## Problem Description\n\n${draft.solution.problem_description}\n\n## Solution\n\n${draft.solution.solution_description}\n\n${separator}\n\n## Assessment \n\n${draft.assessment_full}`; // Define a unique filename for each draft const fileName = `${draft.type}_${index + 1}.txt`; zip.file(fileName, fileContent); }); // 5. Generate the complete zip file as a "blob" // This is an asynchronous operation, so we use .then() or await try { const content = await zip.generateAsync({ type: "blob" }); // 6. Trigger the download in the browser // Create a temporary link element const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = "drafts_export.zip"; // The name of the downloaded zip file // Append to the document, click, and then remove document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log("ZIP file generated and download triggered."); } catch (error) { console.error("Error exporting drafts to zip file:", error); } }