559 lines
22 KiB
Vue
559 lines
22 KiB
Vue
<template>
|
|
<div v-if="loading" class="flex justify-center">
|
|
<ProgressSpinner style="width: 50px; height: 50px; margin-top: 50px" strokeWidth="3" fill="transparent" />
|
|
</div>
|
|
<div v-else>
|
|
<div class="flex items-center justify-between p-2">
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading_data" class="flex justify-center">
|
|
<ProgressSpinner style="width: 30px; height: 30px; margin: 30px" strokeWidth="6" fill="transparent" />
|
|
</div>
|
|
<div v-if="data_loaded">
|
|
<Panel class="mt-6">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-bold">Execution Input for ID {{ execution_id }}</span>
|
|
</div>
|
|
</template>
|
|
<template #icons>
|
|
<div v-if="updateLoading" class="flex justify-end">
|
|
<Rating :modelValue="rating" :stars="5" @change="updateRating($event)" />
|
|
</div>
|
|
<div v-else class="flex justify-end">
|
|
<Rating :modelValue="rating" :stars="5" :readonly="true" @change="updateRating($event)" />
|
|
</div>
|
|
</template>
|
|
<div class="box p-4 border rounded-md shadow-sm" style="background-color: white">
|
|
<table class="table-auto w-full border-collapse border border-gray-300">
|
|
<tbody>
|
|
<tr v-for="(input, index) in filteredInputs" :key="index">
|
|
<th v-if="index === 'MultiFileUpload'" class="border border-gray-300 px-4 py-2">Files Uploaded</th>
|
|
<th v-else-if="index === 'SingleFileUpload'" class="border border-gray-300 px-4 py-2 bg-gray-500 text-white">Parameter</th>
|
|
<th v-else-if="index === 'input_multiselect_name'">
|
|
{{ scenario.inputs && Array.isArray(scenario.inputs) ? scenario.inputs.find((i) => i.name === 'input_multiselect')?.label : null }}
|
|
</th>
|
|
<th v-else class="border border-gray-300 px-4 py-2">
|
|
{{ index.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) }}
|
|
</th>
|
|
<td class="border border-gray-300 px-4 py-2">
|
|
<div v-if="index === 'MultiFileUpload'">
|
|
{{ filteredInputs.SingleFileUpload.replace(/\\/g, '/').split('/').pop() }}
|
|
<Button icon="pi pi-download" class="p-button-text p-button-sm" label="Download" @click="downloadFile(inputs['SingleFileUpload'])" />
|
|
</div>
|
|
<div v-else-if="index !== 'SingleFileUpload'">{{ input }}</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="data_loaded && scenario.chatEnabled && exec_scenario.latestStepStatus != 'ERROR'" class="flex justify-center">
|
|
<div v-if="!chat_enabled" class="flex gap-4 mt-4">
|
|
<Button label="Open Chat" @click="chatEnabled" size="large" iconPos="right" icon="pi pi-comments"></Button>
|
|
</div>
|
|
<div v-else class="flex gap-4 mt-4">
|
|
<Button label="Return to scenario" @click="chatDisabled" size="large" iconPos="right" icon="pi pi-backward"></Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Panel>
|
|
|
|
<div v-if="!chat_enabled" class="mt-4">
|
|
<Panel class="mt-6">
|
|
<template #header>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-bold">Workflow response</span>
|
|
</div>
|
|
</template>
|
|
<template #icons>
|
|
<div class="flex justify-end">
|
|
<Button severity="secondary" rounded @click="openDebug" v-tooltip.left="'View code'">
|
|
<i class="pi pi-code"></i>
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
<div v-if="exec_scenario.latestStepStatus == 'ERROR'" class="card flex flex-col gap-4 w-full">
|
|
<div v-if="exec_scenario.latestStepOutput">
|
|
<p class="text-red-500 font-bold">Error: {{ exec_scenario.latestStepOutput }}</p>
|
|
</div>
|
|
<div v-else>
|
|
<p class="text-red-500 font-bold">Error: Execution failed.</p>
|
|
</div>
|
|
</div>
|
|
<div v-if="exec_scenario.latestStepStatus != 'ERROR'" class="card flex flex-col gap-4 w-full">
|
|
<div v-if="scenario.outputType == 'ciaOutput'">
|
|
<ChangeImpactOutputViewer :scenario_output="scenario_output" />
|
|
</div>
|
|
<div v-else-if="loadingStore.exectuion_loading && loadingStore.getExecIdLoading === execution_id">
|
|
<div class="flex justify-center mt-4">
|
|
<jellyfish-loader :loading="loadingStore.exectuion_loading" scale="1" color="#A100FF" />
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div v-if="fileType == 'FILE' && exec_scenario.execSharedMap.status != null && exec_scenario.execSharedMap.status === 'DONE'">
|
|
<ul class="file-list">
|
|
<li class="file-item">
|
|
sf_document-{{ execution_id }}
|
|
<Button icon="pi pi-download" class="p-button-text p-button-sm" label="Download" @click="downloadFile(inputs['SingleFileUpload'])" />
|
|
</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>
|
|
|
|
<Dialog v-model:visible="debug_modal" maximizable modal :header="scenario.name" :style="{ width: '75%' }" :breakpoints="{ '1199px': '75vw', '575px': '90vw' }">
|
|
<div class="flex">
|
|
<div class="card flex flex-col gap-4 w-full">
|
|
<JsonEditorVue v-model="exec_scenario" />
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
<div v-else="chat_enabled" class="mt-4">
|
|
<Panel class="mt-6">
|
|
<template #header>
|
|
<div class="flex items-center gap-2 mt-2">
|
|
<span class="font-bold">Chat with WizardAI</span>
|
|
</div>
|
|
</template>
|
|
<div class="card flex flex-col gap-4 w-full">
|
|
<ChatClient :scenarioExecutionId="execution_id" />
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import ChangeImpactOutputViewer from '@/components/ChangeImpactOutputViewer.vue';
|
|
import ChatClient from '@/components/ChatClient.vue';
|
|
import { LoadingStore } from '@/stores/LoadingStore.js';
|
|
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 ProgressSpinner from 'primevue/progressspinner';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { JellyfishLoader } from 'vue3-spinner';
|
|
import { ScenarioService } from '../../service/ScenarioService.js';
|
|
import { ScenarioExecutionStore } from '../../stores/ScenarioExecutionStore.js';
|
|
import { UserPrefStore } from '../../stores/UserPrefStore.js';
|
|
|
|
const loadingStore = LoadingStore();
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const value = ref('');
|
|
const scenario = ref({});
|
|
const scenario_output = ref(null);
|
|
const loading = ref(false);
|
|
const data_loaded = ref(false);
|
|
const loading_data = ref(false);
|
|
const formData = ref({});
|
|
const exec_id = ref(null);
|
|
const exec_scenario = ref({});
|
|
const debug_modal = ref(false);
|
|
const rating = ref(null);
|
|
const scenario_execution_store = ScenarioExecutionStore();
|
|
const execution = scenario_execution_store.getSelectedExecScenario;
|
|
const execution_id = ref(null);
|
|
const inputs = ref(null);
|
|
const steps = ref(null);
|
|
const toast = useToast();
|
|
const fileNames = ref([]); // Memorizza i nomi dei file nello zip
|
|
const fileNamesOutput = ref([]); // Memorizza i nomi dei file nello zip
|
|
const zipInput = ref(null); // Contenitore per il file zip
|
|
const zipOutput = ref(null); // Contenitore per il file zip
|
|
const userPrefStore = UserPrefStore();
|
|
const updateLoading = ref(false);
|
|
const fileContent = ref('');
|
|
const fileType = ref('');
|
|
const chat_enabled = ref(false);
|
|
const baseUploadDir = 'mnt/hermione_storage/documents/file_input_scenarios/';
|
|
|
|
onMounted(() => {
|
|
if (execution) {
|
|
execution_id.value = execution.id;
|
|
} else {
|
|
const url = window.location.href;
|
|
execution_id.value = new URL(url).searchParams.get('id');
|
|
}
|
|
retrieveScenarioExec(execution_id.value);
|
|
});
|
|
|
|
const retrieveScenarioExec = (id) => {
|
|
loading.value = true;
|
|
|
|
axios.get('/execution?id=' + id).then((response) => {
|
|
loading.value = false;
|
|
scenario.value = response.data.scenario;
|
|
exec_scenario.value = response.data;
|
|
data_loaded.value = true;
|
|
rating.value = response.data.rating;
|
|
scenario_output.value = response.data.execSharedMap.scenario_output;
|
|
exec_id.value = response.data.scenarioExecution_id;
|
|
inputs.value = response.data.scenarioExecutionInput.inputs;
|
|
steps.value = response.data.scenario.steps;
|
|
if (inputs.value['MultiFileUpload']) {
|
|
if (steps.value[0].attributes['codegenie_output_type']) {
|
|
extractFiles(inputs.value['MultiFileUpload'], 'input', zipInput);
|
|
if (steps.value[0].attributes['codegenie_output_type'] == 'FILE') {
|
|
fileType.value = 'FILE';
|
|
} else if (steps.value[0].attributes['codegenie_output_type'] == 'MARKDOWN') {
|
|
fileType.value = 'MARKDOWN';
|
|
showFileContent(scenario_output.value, 'MARKDOWN');
|
|
} else if (steps.value[0].attributes['codegenie_output_type'] == 'JSON') {
|
|
fileType.value = 'JSON';
|
|
showFileContent(scenario_output.value, 'JSON');
|
|
}
|
|
}
|
|
}
|
|
if (exec_scenario.value.executedByUsername === userPrefStore.getUser.username) {
|
|
updateLoading.value = true;
|
|
}
|
|
});
|
|
};
|
|
const filteredInputs = computed(() => {
|
|
const { input_multiselect_id, ...rest } = inputs.value;
|
|
return rest;
|
|
});
|
|
|
|
const extractFiles = async (base64String, type, zip) => {
|
|
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)
|
|
if (type == 'input') {
|
|
fileNames.value = getFileNamesInput(zipData);
|
|
} else {
|
|
fileNamesOutput.value = getFileNames(zipData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error extracting zip:', error);
|
|
if (type == 'input') {
|
|
fileNames.value = [];
|
|
} else {
|
|
fileNamesOutput.value = [];
|
|
}
|
|
}
|
|
};
|
|
|
|
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 = 'File type not supported.';
|
|
}
|
|
} catch (error) {
|
|
fileContent.value = 'Error while parsing the file.';
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
// 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
|
|
const fileName = relativePath.split('/').pop(); // Estrai solo il nome del file
|
|
files.push(fileName); // Aggiungiamo solo il nome del file
|
|
}
|
|
});
|
|
|
|
return files;
|
|
};
|
|
|
|
const getFileNamesInput = (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;
|
|
};
|
|
|
|
// Funzione per scaricare il file
|
|
const downloadFileInput = async (fileName) => {
|
|
if (!zipInput.value) return;
|
|
|
|
try {
|
|
// Estrai il file dallo zip
|
|
const fileContent = await zipInput.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);
|
|
}
|
|
};
|
|
|
|
const downloadFileOutput = async (fileName) => {
|
|
if (!zipOutput.value) return;
|
|
|
|
try {
|
|
// Estrai il file dallo zip
|
|
const fileContent = await zipOutput.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);
|
|
}
|
|
};
|
|
|
|
const downloadFile = async (filePath) => {
|
|
try {
|
|
let relativePath = filePath;
|
|
if (filePath.startsWith(baseUploadDir)) {
|
|
relativePath = filePath.substring(baseUploadDir.length);
|
|
}
|
|
|
|
console.log('Original path:', filePath);
|
|
console.log('Relative path:', relativePath);
|
|
|
|
// Chiamata all'API backend per ottenere il file
|
|
await scenario_execution_store.downloadFile(relativePath, execution_id.value);
|
|
} catch (error) {
|
|
console.error('Error downloading file:', error);
|
|
|
|
// Notifica di errore
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'Error downloading file. Please try again.',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const downloadFolderFromBase64 = async (base64String) => {
|
|
try {
|
|
// Decodifica la stringa base64 in un array di byte
|
|
const binaryString = atob(base64String); // Decodifica base64 in una stringa binaria
|
|
const byteArray = new Uint8Array(binaryString.length); // Crea un array di byte
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
byteArray[i] = binaryString.charCodeAt(i); // Popola l'array di byte
|
|
}
|
|
|
|
// Crea un oggetto JSZip per lavorare con il contenuto ZIP
|
|
const zip = await JSZip.loadAsync(byteArray); // Carica direttamente l'array di byte
|
|
|
|
// Estrai il primo nome della cartella presente nello ZIP
|
|
let folderName = Object.keys(zip.files).find((file) => zip.files[file].dir); // Trova il primo file che è una cartella
|
|
|
|
// Se non è stata trovata alcuna cartella, creiamo una cartella chiamata "folderToDownload"
|
|
if (!folderName) {
|
|
folderName = 'docOutput-' + execution_id.value;
|
|
console.log(`No folders founded in the ZIP. Creating a new folder: "${folderName}".`);
|
|
}
|
|
|
|
// Crea un nuovo archivio ZIP in cui mettere i file della cartella
|
|
const newZip = new JSZip();
|
|
|
|
// Se una cartella esiste nello ZIP, estrai i file da essa, altrimenti crea una nuova cartella
|
|
const folder = zip.folder(folderName);
|
|
if (folder) {
|
|
// Aggiungi ogni file della cartella al nuovo archivio ZIP
|
|
const files = folder.files;
|
|
for (const fileName in files) {
|
|
const file = files[fileName];
|
|
|
|
// Controlla se il file è valido (non nullo)
|
|
if (file && file.async) {
|
|
try {
|
|
const fileContent = await file.async('blob'); // Estrai il contenuto del file
|
|
newZip.file(fileName, fileContent);
|
|
} catch (fileError) {
|
|
console.error(`Error while extracting "${fileName}":`, fileError);
|
|
}
|
|
} else {
|
|
console.warn(`"${fileName}" is invalid or cannot be elaborated`);
|
|
}
|
|
}
|
|
} else {
|
|
// Se la cartella non esiste, crea una cartella vuota con nome "folderToDownload"
|
|
newZip.folder(folderName);
|
|
}
|
|
|
|
// Crea il nuovo file ZIP da scaricare
|
|
const newZipContent = await newZip.generateAsync({ type: 'blob' });
|
|
const url = URL.createObjectURL(newZipContent);
|
|
|
|
// Crea un link per scaricare il file ZIP
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${folderName}.zip`; // Nome del file ZIP che contiene la cartella
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
|
|
URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error('Error while downloading folder:', error);
|
|
}
|
|
};
|
|
|
|
async function updateRating(newRating) {
|
|
loading_data.value = true;
|
|
ScenarioService.updateScenarioExecRating(execution_id.value, newRating.value)
|
|
.then((response) => {
|
|
console.log('response:', response);
|
|
if (response.data === 'OK') {
|
|
rating.value = newRating.value;
|
|
console.log('Rating successfully updated:', response.data);
|
|
toast.add({
|
|
severity: 'success', // Tipo di notifica (successo)
|
|
summary: 'Success', // Titolo della notifica
|
|
detail: 'Rating updated with success.', // Messaggio dettagliato
|
|
life: 3000 // Durata della notifica in millisecondi
|
|
});
|
|
} else {
|
|
console.error('Error while updating rating', response.data);
|
|
toast.add({
|
|
severity: 'error', // Tipo di notifica (errore)
|
|
summary: 'Error', // Titolo della notifica
|
|
detail: 'Error updating rating. Try later.', // Messaggio dettagliato
|
|
life: 3000 // Durata della notifica in millisecondi
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error while calling backend:', error);
|
|
})
|
|
.finally(() => {
|
|
loading_data.value = false;
|
|
});
|
|
}
|
|
|
|
const back = () => {
|
|
router.push({ name: 'scenario-list' });
|
|
};
|
|
|
|
const openDebug = () => {
|
|
debug_modal.value = true;
|
|
};
|
|
|
|
const chatEnabled = () => {
|
|
chat_enabled.value = true;
|
|
};
|
|
|
|
const chatDisabled = () => {
|
|
chat_enabled.value = false;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.input-container {
|
|
margin-bottom: 1em;
|
|
}
|
|
|
|
.input-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.full-width-input {
|
|
width: 100%;
|
|
}
|
|
|
|
.editor ol {
|
|
list-style-type: decimal !important;
|
|
}
|
|
|
|
.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>
|