Spaces:
Sleeping
Sleeping
| 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 = `<option value="">${defaultText}</option>`; | |
| 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 = ` | |
| <input type="checkbox" class="${filterType}-checkbox option-checkbox" id="${safeId}" value="${option}"> | |
| <span>${option}</span> | |
| `; | |
| 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 = `<a data-value="">Tous</a>`; | |
| 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 = `<a data-value="${opt}">${opt}</a>`; | |
| 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 = ` | |
| <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> | |
| `; | |
| // 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 => `<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 3GPP requirements | |
| </h4> | |
| <ul class="list-disc pl-5 space-y-1 text-gray-700 text-xs"> | |
| ${reqItemsUl} | |
| </ul> | |
| ` | |
| // 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 = ` | |
| <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> | |
| `; | |
| // 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 = `<p class="text-xs text-gray-700 leading-relaxed">${solution['solution_description'] || 'No available solution description'}</p>`; | |
| } | |
| 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 = `<span class="step-icon">${state.type == "draft" ? "📝" : `🔎`}</span>${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 = `<p>No drafted solutions for now</p>` | |
| 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 = ` | |
| <input type="checkbox" id="${insight.id}" ${isChecked} class="checkbox checkbox-primary" /> | |
| <span class="label-text">${insight.text}</span> | |
| `; | |
| // 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); | |
| } | |
| } |