Lucas ARRIESSE
Include searched FTO report
adf98e8
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);
}
}