Requisito Toscano

This commit is contained in:
Florinda
2025-01-29 11:08:58 +01:00
parent 36a9811f05
commit b590508ca4
8 changed files with 1146 additions and 77 deletions

View File

@@ -4,34 +4,175 @@
<h1>
{{ scenario.name }}
</h1>
</div>
<div class="flex mt-6">
<div class="card flex flex-col gap-4 w-full">
<template v-if="scenario.inputs && scenario.inputs.length === 1">
<div class="input-container flex items-center">
<div class="flex-grow">
<label :for="scenario.inputs[0].name"><h2>{{ scenario.inputs[0].label }}</h2></label>
<div class="input-wrapper">
<component
:is="getInputComponent(scenario.inputs[0].type)"
:id="scenario.inputs[0].name"
v-model="formData[scenario.inputs[0].name]"
:options="scenario.inputs[0].options"
class="full-width-input"
/>
</div>
</div>
</div>
<div class="flex justify-center">
<Button :disabled="loadingStore.exectuion_loading || !isInputFilled" label="Execute" @click="execScenario" size="large" iconPos="right" icon="pi pi-cog"></Button>
</div>
</template>
<template v-else>
<h3>
{{ scenario.description }}
</h3>
<template v-if="scenario.inputs">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="input in scenario.inputs" :key="input.name" class="input-container">
<label :for="input.name"><b>{{ input.label }}</b></label>
<div class="input-wrapper">
<div v-for="input in scenario.inputs" :key="input.name"
:class="['input-container', input.type === 'textarea' ? 'col-span-12' : '']">
<div v-if="input.type === 'singlefile'">
<label :for="input.name">
<b>{{ input.label }}</b>
<i
class="pi pi-info-circle text-violet-600 cursor-pointer"
v-tooltip="'Upload one PR document of .docx type. Mandatory if you want to execute scenario.'"
></i>
</label>
<div>
<FileUpload
:name="'MultiFileUpload'"
:customUpload="false"
:url="uploadUrlPR"
@upload="(event) => onUpload(event, 'SingleFileUpload')"
:multiple="false"
accept=".docx"
auto
:showUploadButton="false"
:showCancelButton="false"
:maxFileSize="10000000"
v-model:files="uploadedFiles"
>
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="pt-4">
<!-- Tabella per file in caricamento -->
<div v-if="uploadedFiles.length > 0">
<table class="table-auto w-full border-collapse border border-gray-200">
<thead>
<tr>
<th class="border border-gray-300 p-2">Name</th>
<th class="border border-gray-300 p-2">Dimension</th>
<th class="border border-gray-300 p-2">Status</th>
<th class="border border-gray-300 p-2">Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="(file, index) in uploadedFiles"
:key="file.name + file.size"
class="hover:bg-gray-50"
>
<td class="border border-gray-300 p-2">{{ file.name }}</td>
<td class="border border-gray-300 p-2">{{ formatSize(file.size) }}</td>
<td class="border border-gray-300 p-2">
<Badge value="UPLOADED" severity="success" />
</td>
<td class="border border-gray-300 p-2">
<Button
label="Remove"
@click="onRemove({ file, index }, removeUploadedFileCallback, 'SingleFileUpload')"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template #empty>
<div class="flex items-center justify-center flex-col">
<i class="pi pi-cloud-upload !border-2 !rounded-full !p-8 !text-4xl !text-muted-color" />
<p class="mt-6 mb-0">Drag and drop files to here to upload.</p>
</div>
</template>
</FileUpload>
</div>
</div>
<div v-else-if="input.type === 'multifile'">
<label :for="input.name">
<b>{{ input.label }}
</b>
<i
class="pi pi-info-circle text-violet-600 cursor-pointer"
v-tooltip="'Upload others documents of .docx, .msg, .text type. Optional.'"
></i>
</label>
<div>
<FileUpload
:name="'MultiFileUpload'"
:customUpload="false"
:url="uploadUrlOther"
@upload="(event) => onUpload(event, 'MultiFileUpload')"
:multiple="true"
accept=".msg,.txt,.docx"
auto
:showUploadButton="false"
:showCancelButton="false"
:maxFileSize="10000000"
v-model:files="uploadedFiles"
>
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="pt-4">
<!-- Tabella per file in caricamento -->
<div v-if="uploadedFiles.length > 0">
<table class="table-auto w-full border-collapse border border-gray-200">
<thead>
<tr>
<th class="border border-gray-300 p-2">Name</th>
<th class="border border-gray-300 p-2">Dimension</th>
<th class="border border-gray-300 p-2">Status</th>
<th class="border border-gray-300 p-2">Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="(file, index) in uploadedFiles"
:key="file.name + file.size"
class="hover:bg-gray-50"
>
<td class="border border-gray-300 p-2">{{ file.name }}</td>
<td class="border border-gray-300 p-2">{{ formatSize(file.size) }}</td>
<td class="border border-gray-300 p-2">
<Badge value="UPLOADED" severity="success" />
</td>
<td class="border border-gray-300 p-2">
<Button
label="Remove"
@click="onRemove({ file, index }, removeUploadedFileCallback, 'MultiFileUpload')"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template #empty>
<div class="flex items-center justify-center flex-col">
<i class="pi pi-cloud-upload !border-2 !rounded-full !p-8 !text-4xl !text-muted-color" />
<p class="mt-6 mb-0">Drag and drop files to here to upload.</p>
</div>
</template>
</FileUpload>
</div>
</div>
<div v-else>
<label :for="input.name"><b>{{ input.label }}</b></label>
<div class="input-wrapper">
<component
:is="getInputComponent(input.type)"
:id="input.name"
@@ -40,13 +181,18 @@
class="full-width-input"
:disabled="loadingStore.exectuion_loading"
/>
</div>
</div>
</div>
</div>
<div class="flex justify-center">
<Button :disabled="loadingStore.exectuion_loading || !isInputFilled " label="Execute" @click="execScenario" size="large" iconPos="right" icon="pi pi-cog"></Button>
</div>
</template>
</div>
</div>
@@ -71,31 +217,76 @@
<div class="flex items-center gap-2">
<span class="font-bold">Workflow Response</span>
</div>
<div>
</template>
<template #icons>
<div class="flex justify-end">
<div class="flex">
<Rating
:modelValue="rating"
:stars="5"
@change="updateRating($event)"
/>
</div>
</template>
<template #icons>
</div>
<div class="flex justify-end">
<div>
<Button severity="secondary" rounded @click="openDebug" v-tooltip.left="'View code'">
<i class="pi pi-code"></i>
</Button>
</div>
</div>
</template>
<div class="card flex flex-col gap-4 w-full">
<div v-if="scenario.outputType == 'ciaOutput'">
<ChangeImpactOutputViewer :scenario_output="scenario_output" />
</div>
<div v-if="scenario.outputType == 'file'">
<Button
icon="pi pi-download"
label="Download File"
class="p-button-primary"
@click="downloadFile"
/>
</div>
<div v-else>
<!-- <div v-if="fileNamesOutput.length">
<ul>
<li v-for="(file, idx) in fileNamesOutput" :key="idx" class="file-item">
{{ file }}
<Button
icon="pi pi-download"
class="p-button-text p-button-sm"
label="Download"
@click="downloadZipFile(file)"
/>
</li>
</ul>
</div> -->
<div v-if="fileType == 'FILE'">
<ul>
<li class="file-item">
sf_document-{{exec_id}}
<Button
icon="pi pi-download"
class="p-button-text p-button-sm"
label="Download"
@click="downloadFile(scenario_output)"
/>
</li>
</ul>
</div>
<div v-else-if="fileType == 'MARKDOWN'">
<div v-html="fileContent" class="markdown-content"></div>
</div>
<div v-else-if="fileType == 'JSON'">
<pre>{{ fileContent }}</pre>
</div>
<div v-else>
<MdPreview class="editor" v-model="scenario_output" language="en-US" />
</div>
</div>
</div>
</Panel>
@@ -119,12 +310,16 @@ import ChangeImpactOutputViewer from '@/components/ChangeImpactOutputViewer.vue'
import { LoadingStore } from '@/stores/LoadingStore';
import axios from 'axios';
import JsonEditorVue from 'json-editor-vue';
import JSZip from 'jszip';
import { marked } from 'marked';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import moment from 'moment';
import { usePrimeVue } from 'primevue/config';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import Textarea from 'primevue/textarea';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -134,7 +329,7 @@ import { ScenarioService } from '../../service/ScenarioService';
const loadingStore = LoadingStore();
const toast = useToast();
const zip = ref(null);
const router = useRouter();
const route = useRoute();
const value = ref('');
@@ -151,6 +346,27 @@ const exec_id = ref(null);
const exec_scenario = ref({});
const debug_modal = ref(false);
let pollingInterval = null;
const folderName = ref("");
const fileNamesOutput = ref([]);
// URL di upload
const uploadUrlBase = import.meta.env.VITE_BACKEND_URL;
const uploadUrl = ref("");
const uploadUrlPR = ref("");
const uploadUrlOther = ref("");
// File che l'utente ha selezionato
const uploadedFiles = ref([]);
const numberPrFiles = ref(0);
// :url="`http://localhost:8081/uploadListFiles/${folderName}`"
// Stato per l'ID univoco della cartella
const uniqueFolderId = ref(generateUniqueId());
const confirm = useConfirm();
const $primevue = usePrimeVue();
const files = ref([]);
const fileContent = ref('');
const fileType = ref('');
const reqMultiFile = ref(false);
let startTime = ref(null);
let timerInterval = ref(null);
@@ -184,6 +400,13 @@ const isInputFilled = computed(() => {
onMounted(() => {
fetchScenario(route.params.id);
const timestamp = Date.now(); // Ottiene il timestamp corrente
const randomNumber = Math.floor(Math.random() * 1000);
folderName.value = `${timestamp}_${randomNumber}`
uploadUrl.value = uploadUrlBase + '/uploadListFiles/' + folderName.value;
uploadUrlPR.value = uploadUrl.value + '/PR';
uploadUrlOther.value = uploadUrl.value + '/OTHER';
console.log("Upload URL:", uploadUrl);
});
// Ricarica i dati quando cambia il parametro `id`
@@ -198,6 +421,11 @@ watch(() => route.params.id, fetchScenario);
axios.get(`/scenarios/${id}`)
.then(response => {
scenario.value = response.data;
console.log("Scenario fetched:", scenario.value);
if (scenario.value.inputs.some((input) => input.name === 'MultiFileUpload' || input.name === 'SingleFileUpload')) {
reqMultiFile.value = true;
}
})
.catch(error => {
console.error("Error fetching scenario:", error);
@@ -221,8 +449,19 @@ watch(() => route.params.id, fetchScenario);
};
const execScenario = () => {
if(numberPrFiles.value!==1 && reqMultiFile.value){
toast.add({
severity: 'warn', // Tipo di notifica (errore)
summary: 'Attention', // Titolo della notifica
detail: 'You can upload only 1 PR file. Please remove others.' // Messaggio dettagliato
});
}else{
loading_data.value = true;
data_loaded.value = false;
rating.value = 0;
startTimer();
loadingStore.exectuion_loading = true;
@@ -234,11 +473,12 @@ watch(() => route.params.id, fetchScenario);
axios.post('/scenarios/execute-async', data)
.then(response => {
console.log("Response data exec 1:", response.data);
scenario_response.value = response.data;
scenario_response_message.value = response.data.message;
scenario_output.value = response.data.stringOutput;
exec_id.value = response.data.scenarioExecution_id
loadingStore.setIdExecLoading(exec_id.value);
// Start polling
startPolling();
})
@@ -246,12 +486,9 @@ watch(() => route.params.id, fetchScenario);
console.error('Error executing scenario:', error);
loadingStore.exectuion_loading = false;
});
}
};
const back = () => {
router.push({ name: 'scenario-list'});
}
const openDebug = () => {
axios.get('/scenarios/execute/'+ exec_id.value).then(resp =>{
@@ -261,6 +498,7 @@ watch(() => route.params.id, fetchScenario);
}
const pollBackendAPI = () => {
axios.get('/scenarios/getExecutionProgress/'+exec_id.value).then(response => {
@@ -272,8 +510,42 @@ watch(() => route.params.id, fetchScenario);
loading_data.value = false;
data_loaded.value = true;
scenario_output.value = response.data.stringOutput;
console.log("Response data exec 2:", response.data);
exec_id.value = response.data.scenarioExecution_id
scenario_response_message.value = null //if != null, next scenario starts with old message
console.log("Scenario 3:", scenario.value);
// Controlla se l'array `inputs` contiene un elemento con `name = 'MultiFileUpload'`
if (scenario.value.inputs.some((input) => input.name === 'MultiFileUpload')) {
console.log('Im in');
// Accedi al primo step e controlla se esiste l'attributo `codegenie_output_type`
const firstStep = scenario.value.steps[0];
if (firstStep?.attributes?.['codegenie_output_type']) {
// Controlla se `codegenie_output_type` è uguale a 'FILE'
// if (firstStep.attributes['codegenie_output_type'] === 'FILE') {
// console.log('base64 ', scenario_output.value);
// Chiama la funzione `extractFiles` con il valore di `scenario_output.value`
//extractFiles(scenario_output.value);
//}
if(firstStep.attributes['codegenie_output_type'] == 'FILE'){
//console.log('base64 ', scenario_output.value)
//extractFiles(scenario_output.value, 'output', zipOutput)
fileType.value = 'FILE'
}
else if(firstStep.attributes['codegenie_output_type'] == 'MARKDOWN'){
fileType.value = 'MARKDOWN'
showFileContent(scenario_output.value, 'MARKDOWN')
}
else if(firstStep.attributes['codegenie_output_type'] == 'JSON'){
fileType.value = 'JSON'
showFileContent(scenario_output.value, 'JSON')
}
}
}
} else {
console.log("Condition not met, polling continues.");
@@ -283,6 +555,37 @@ watch(() => route.params.id, fetchScenario);
});
}
const showFileContent = (base64String, type) => {
try {
// Decodifica la stringa Base64
const binaryString = atob(base64String);
const binaryLength = binaryString.length;
const bytes = new Uint8Array(binaryLength);
for (let i = 0; i < binaryLength; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Converti i byte in una stringa leggibile
const textContent = new TextDecoder().decode(bytes);
// Gestione del tipo di file
if (type === 'MARKDOWN') {
//fileType.value = 'markdown';
fileContent.value = marked(textContent); // Converte Markdown in HTML
} else if (type === 'JSON') {
//fileType.value = 'json';
const jsonObject = JSON.parse(textContent); // Parse JSON
fileContent.value = JSON.stringify(jsonObject, null, 2); // Formatta JSON
} else {
fileContent.value = 'Tipo di file non supportato.';
}
} catch (error) {
fileContent.value = 'Errore durante la decodifica o il parsing del file.';
console.error(error);
}
};
// Function to start polling
function startPolling() {
// Set polling interval (every 2.5 seconds in this case)
@@ -294,11 +597,77 @@ watch(() => route.params.id, fetchScenario);
function stopPolling() {
clearInterval(pollingInterval);
loadingStore.exectuion_loading = false;
loadingStore.setIdExecLoading("");
console.log("Polling stopped.");
}
const extractFiles = async (base64String) => {
try {
// Decodifica la base64 in un array di byte
const byteCharacters = atob(base64String);
const byteNumbers = Array.from(byteCharacters, char => char.charCodeAt(0));
const byteArray = new Uint8Array(byteNumbers);
// Carica il file zip con JSZip
const zipData = await JSZip.loadAsync(byteArray);
zip.value = zipData;
// Ottieni tutti i file (compresi quelli nelle sottocartelle)
fileNamesOutput.value = getFileNames(zipData);
} catch (error) {
console.error('Error extracting zip:', error);
fileNamesOutput.value = [];
}
};
// Funzione ricorsiva per ottenere tutti i file (anche quelli dentro le cartelle)
const getFileNames = (zipData) => {
const files = [];
// Esplora tutti i file nel file zip, considerando anche le sottocartelle
zipData.forEach((relativePath, file) => {
if (!file.dir) { // Escludiamo le cartelle
files.push(relativePath); // Aggiungiamo il percorso relativo del file
}
});
return files;
};
// const uploadSingleFile = async (file) => {
// // Logica per caricare un singolo file utilizzando axios
// try {
// const formData = new FormData();
// formData.append('MultiFileUpload', file);
// const response = await axios.post(uploadUrl.value, formData, {
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// });
// if (response.status === 200) {
// toast.add({ severity: 'success', summary: 'Success', detail: `File "${file.name}" uploaded successfully.`, life: 3000 });
// } else {
// toast.add({ severity: 'error', summary: 'Error', detail: `Failed to upload file "${file.name}".`, life: 3000 });
// }
// } catch (error) {
// toast.add({ severity: 'error', summary: 'Error', detail: `Error uploading file "${file.name}": ${error.message}`, life: 3000 });
// console.error('Error uploading file:', error);
// }
// };
// const removeFile = (file, index, callback) => {
// // Logica per rimuovere un file dalla lista dei "pending"
// console.log('Removing pending file:', file);
// callback(file, index);
// };
async function updateRating(newRating) {
ScenarioService.updateScenarioExecRating(exec_id.value,newRating.value).then((response) => {
@@ -309,7 +678,7 @@ watch(() => route.params.id, fetchScenario);
console.log('Rating aggiornato con successo:', response.data);
toast.add({
severity: 'success', // Tipo di notifica (successo)
summary: 'Successo', // Titolo della notifica
summary: 'Success', // Titolo della notifica
detail: 'Rating updated with success.', // Messaggio dettagliato
life: 3000 // Durata della notifica in millisecondi
});
@@ -317,7 +686,7 @@ watch(() => route.params.id, fetchScenario);
console.error('Errore nell\'aggiornamento del rating', response.data);
toast.add({
severity: 'error', // Tipo di notifica (errore)
summary: 'Errore', // Titolo della notifica
summary: 'Error', // Titolo della notifica
detail: 'Error updating rating. Try later.', // Messaggio dettagliato
life: 3000 // Durata della notifica in millisecondi
});
@@ -325,10 +694,220 @@ watch(() => route.params.id, fetchScenario);
}).catch((error) => {
console.error('Errore durante la chiamata al backend:', error);
})
}
// Funzione per generare un ID univoco
function generateUniqueId() {
return Date.now(); // Puoi usare anche UUID.randomUUID() o una libreria simile
}
const onRemove = (event, removeUploadedFileCallback, type) => {
const { file, index } = event;
console.log('Removing file:', folderName.value);
try {
axios.post(
`/deleteFile`,
{ fileName: file.name, folderName: folderName.value }, // Invio nome del file come payload
{
headers: {
'Content-Type': 'application/json',
},
}
).then(response => {
if (response.status === 200) {
console.log('File removed successfully:', response.data);
// Mostra notifica di successo
toast.add({
severity: "success",
summary: "Success",
detail: "File removed successfully!",
life: 3000,
});
if(type === 'SingleFileUpload'){
numberPrFiles.value -= 1;
console.log("Number of PR files: ", numberPrFiles.value);
}
// Aggiorna lista dei file caricati
removeUploadedFileCallback(index);
} else {
console.error('Failed to remove file:', response.statusText);
// Mostra notifica di errore
toast.add({
severity: "error",
summary: "Error",
detail: `Failed to remove file. Status: ${response.statusText}`,
life: 3000,
});
}
}).catch(error => {
console.error('Error while removing file:', error);
// Mostra notifica di errore
toast.add({
severity: "error",
summary: "Error",
detail: `Error while removing file: ${error.message}`,
life: 3000,
});
});
} catch (error) {
console.error('Error while removing file:', error);
}
};
// const onRemove = (event) => {
// // Metodo per gestire la rimozione dei file
// console.log('Removing file:', folderName.value);
// try {
// axios.post(
// `http://localhost:8081/deleteFile`,
// { fileName: event.file.name, folderName: folderName.value}, // Invio nome del file come payload
// {
// headers: {
// 'Content-Type': 'application/json',
// },
// }
// ).then(response => {
// if (response.status === 200) {
// console.log('File removed successfully:', response.data);
// toast.add({
// severity: "success",
// summary: "Successo",
// detail: "File removed successfully!",
// life: 3000,
// });
// } else {
// console.error('Failed to remove file:', response.statusText);
// toast.add({
// severity: "error",
// summary: "Errore",
// detail: `Failed to remove file. Status: ${xhr.status}`,
// life: 3000,
// });
// }
// })
// } catch (error) {
// console.error('Error while removing file:', error);
// }
// }
const onUpload = (event, uploadType) => {
console.log("response upload ",event.xhr.response);
const { xhr } = event; // Estraggo l'oggetto XMLHttpRequest
if (xhr.status === 200) {
// Risposta OK
if(uploadType==='SingleFileUpload'){
formData.value['SingleFileUpload'] = "OK";
numberPrFiles.value += 1;
console.log("Number of PR files: ", numberPrFiles.value);
}
formData.value['MultiFileUpload'] = xhr.response;
console.log("Form value upload ",formData.value['MultiFileUpload']);
console.log("Upload completato con successo. Risposta:", xhr.response);
toast.add({
severity: "success",
summary: "Success",
detail: "File uploaded successfully!",
life: 3000,
});
} else {
// Errore durante l'upload
console.error("Errore durante l'upload. Stato:", xhr.status, "Risposta:", xhr.response);
toast.add({
severity: "error",
summary: "Error",
detail: `Failed to upload file. Stato: ${xhr.status}`,
life: 3000,
});
}
}
// Funzione per scaricare il file
const downloadZipFile = async (fileName) => {
if (!zip.value) return;
try {
// Estrai il file dallo zip
const fileContent = await zip.value.file(fileName).async('blob');
const url = URL.createObjectURL(fileContent);
// Crea un link per scaricare il file
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error(`Error downloading file "${fileName}":`, error);
}
};
function downloadFile() {
try {
// Converti la stringa base64 in un blob
const base64String = this.scenario_output;
const byteCharacters = atob(base64String);
const byteNumbers = Array.from(byteCharacters, char => char.charCodeAt(0));
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray]);
// Crea un link temporaneo per il download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sf_document-'+exec_id.value+'.docx';// Specifica il nome del file
document.body.appendChild(a);
a.click();
// Rimuovi il link temporaneo
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Errore durante il download del file:', error);
}
}
const formatSize = (bytes) => {
const k = 1024;
const sizes = $primevue.config.locale.fileSizeTypes;
if (bytes === 0) {
return `0 ${sizes[0]}`;
}
const i = Math.floor(Math.log(bytes) / Math.log(k));
const truncatedSize = Math.trunc(bytes / Math.pow(k, i)); // Troncamento del valore
return `${truncatedSize} ${sizes[i]}`;
};
</script>
<style scoped>
@@ -355,5 +934,33 @@ watch(() => route.params.id, fetchScenario);
.editor ul {
list-style-type: disc !important;
}
pre {
white-space: pre-wrap; /* Fa andare a capo il contenuto automaticamente */
word-wrap: break-word; /* Interrompe le parole troppo lunghe */
overflow-wrap: break-word; /* Per compatibilità con più browser */
max-width: 100%; /* Imposta una larghezza massima pari al contenitore genitore */
overflow-x: auto; /* Aggiunge uno scorrimento orizzontale solo se necessario */
background-color: #f5f5f5; /* Colore di sfondo opzionale per migliorare leggibilità */
padding: 10px; /* Spaziatura interna */
border-radius: 5px; /* Bordo arrotondato opzionale */
font-family: monospace; /* Font specifico per codice */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Ombra per migliorare estetica */
}
.markdown-content {
white-space: pre-wrap; /* Gestisce correttamente gli spazi e i ritorni a capo */
word-wrap: break-word; /* Spezza le parole lunghe */
overflow-wrap: break-word; /* Per compatibilità con più browser */
max-width: 100%; /* Adatta il contenuto alla larghezza del contenitore */
overflow-x: auto; /* Aggiunge scorrimento orizzontale solo se necessario */
background-color: #f5f5f5; /* Sfondo per distinguere il contenuto */
padding: 10px; /* Margini interni */
border-radius: 5px; /* Bordo arrotondato */
font-family: Arial, sans-serif; /* Puoi scegliere un font leggibile */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Effetto estetico di ombra */
line-height: 1.5; /* Aumenta la leggibilità */
}
</style>