feat: Add Execution Response Section component and related composables for file handling and error management
- Implemented ExecutionResponseSection.vue to display execution results and handle file downloads. - Created useBase64Decoder.js for base64 decoding utilities. - Developed useChatToggle.js for managing chat panel state. - Added useErrorHandler.js for standardized error handling with toast notifications. - Introduced useFileDownload.js for various file download operations. - Created useFileProcessing.js for processing files, including zip extraction and content display. - Implemented usePolling.js for polling backend API for execution status. - Added useScenarioRating.js for managing scenario execution ratings. - Developed CSV export utilities in csvExport.js for generating and downloading CSV files. - Created formDataProcessor.js for processing and validating multiselect form data. - Implemented inputComponents.js for mapping input types to PrimeVue components. - Enhanced ScenarioExecHistory.vue to integrate new components and functionalities.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
<script setup>
|
||||
import ChatClient from '@/components/ChatClient.vue';
|
||||
import ExecutionInputTable from '@/components/ExecutionInputTable.vue';
|
||||
import OldExecutionResponsePanel from '@/components/OldExecutionResponsePanel.vue';
|
||||
import { LoadingStore } from '@/stores/LoadingStore.js';
|
||||
import { ScenarioExecutionStore } from '@/stores/ScenarioExecutionStore.js';
|
||||
import { UserPrefStore } from '@/stores/UserPrefStore.js';
|
||||
import { ScenarioService } from '@/service/ScenarioService.js';
|
||||
import axios from 'axios';
|
||||
import JSZip from 'jszip';
|
||||
import { marked } from 'marked';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
// ============= Stores and Services =============
|
||||
const loadingStore = LoadingStore();
|
||||
const scenarioExecutionStore = ScenarioExecutionStore();
|
||||
const userPrefStore = UserPrefStore();
|
||||
const toast = useToast();
|
||||
|
||||
// ============= Reactive State =============
|
||||
const scenario = ref({});
|
||||
const exec_scenario = ref({});
|
||||
const scenario_output = ref(null);
|
||||
const inputs = ref(null);
|
||||
const steps = ref(null);
|
||||
const execution_id = ref(null);
|
||||
const rating = ref(null);
|
||||
const loading = ref(false);
|
||||
const data_loaded = ref(false);
|
||||
const loading_data = ref(false);
|
||||
const chat_enabled = ref(false);
|
||||
const updateLoading = ref(false);
|
||||
|
||||
// ============= File State =============
|
||||
const fileContent = ref('');
|
||||
const fileType = ref('');
|
||||
const zipInput = ref(null);
|
||||
const fileNames = ref([]);
|
||||
const fileNamesOutput = ref([]);
|
||||
|
||||
// ============= Constants =============
|
||||
const baseUploadDir = '/mnt/hermione_storage/hermione/file_input_scenarios/';
|
||||
|
||||
// ============= Lifecycle Hooks =============
|
||||
onMounted(() => {
|
||||
const execution = scenarioExecutionStore.getSelectedExecScenario;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// ============= Data Fetching Methods =============
|
||||
const retrieveScenarioExec = async (id) => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/execution?id=' + id);
|
||||
|
||||
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;
|
||||
inputs.value = response.data.scenarioExecutionInput.inputs;
|
||||
steps.value = response.data.scenario.steps;
|
||||
|
||||
handleFileProcessing();
|
||||
|
||||
if (exec_scenario.value.executedByUsername === userPrefStore.getUser.username) {
|
||||
updateLoading.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrieving scenario execution:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= File Processing Methods =============
|
||||
const handleFileProcessing = () => {
|
||||
if (inputs.value['MultiFileUpload'] && steps.value[0]?.attributes?.['codegenie_output_type']) {
|
||||
extractFiles(inputs.value['MultiFileUpload'], 'input', zipInput);
|
||||
|
||||
const outputType = steps.value[0].attributes['codegenie_output_type'];
|
||||
fileType.value = outputType;
|
||||
|
||||
if (outputType === 'MARKDOWN') {
|
||||
showFileContent(scenario_output.value, 'MARKDOWN');
|
||||
} else if (outputType === 'JSON') {
|
||||
showFileContent(scenario_output.value, 'JSON');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const extractFiles = async (base64String, type, zip) => {
|
||||
try {
|
||||
const byteCharacters = atob(base64String);
|
||||
const byteNumbers = Array.from(byteCharacters, (char) => char.charCodeAt(0));
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
const zipData = await JSZip.loadAsync(byteArray);
|
||||
zip.value = zipData;
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
const textContent = new TextDecoder().decode(bytes);
|
||||
|
||||
if (type === 'MARKDOWN') {
|
||||
fileContent.value = marked(textContent);
|
||||
} else if (type === 'JSON') {
|
||||
const jsonObject = JSON.parse(textContent);
|
||||
fileContent.value = JSON.stringify(jsonObject, null, 2);
|
||||
} else {
|
||||
fileContent.value = 'File type not supported.';
|
||||
}
|
||||
} catch (error) {
|
||||
fileContent.value = 'Error while parsing the file.';
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileNames = (zipData) => {
|
||||
const files = [];
|
||||
zipData.forEach((relativePath, file) => {
|
||||
if (!file.dir) {
|
||||
const fileName = relativePath.split('/').pop();
|
||||
files.push(fileName);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
const getFileNamesInput = (zipData) => {
|
||||
const files = [];
|
||||
zipData.forEach((relativePath, file) => {
|
||||
if (!file.dir) {
|
||||
files.push(relativePath);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
// ============= Download Methods =============
|
||||
const downloadFile = async (filePath) => {
|
||||
try {
|
||||
let relativePath = filePath;
|
||||
if (filePath.startsWith(baseUploadDir)) {
|
||||
relativePath = filePath.substring(baseUploadDir.length);
|
||||
}
|
||||
|
||||
await scenarioExecutionStore.downloadFile(relativePath, execution_id.value);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Error downloading file. Please try again.',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCodegenieFile = (base64String) => {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'sf_document-' + execution_id.value + '.docx';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ============= Rating Methods =============
|
||||
const updateRating = async (newRating) => {
|
||||
loading_data.value = true;
|
||||
|
||||
try {
|
||||
const response = await ScenarioService.updateScenarioExecRating(execution_id.value, newRating.value);
|
||||
|
||||
if (response.data === 'OK') {
|
||||
rating.value = newRating.value;
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Rating updated with success.',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Error updating rating. Try later.',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while calling backend:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Error updating rating.',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
loading_data.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= Chat Methods =============
|
||||
const chatEnabled = () => {
|
||||
chat_enabled.value = true;
|
||||
};
|
||||
|
||||
const chatDisabled = () => {
|
||||
chat_enabled.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Loading Spinner -->
|
||||
<div v-if="loading" class="flex justify-center">
|
||||
<ProgressSpinner style="width: 50px; height: 50px; margin-top: 50px" strokeWidth="3" fill="transparent" />
|
||||
</div>
|
||||
|
||||
<div v-if="loading_data" class="flex justify-center">
|
||||
<ProgressSpinner style="width: 30px; height: 30px; margin: 30px" strokeWidth="6" fill="transparent" />
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div v-if="data_loaded">
|
||||
<!-- Execution Input Panel -->
|
||||
<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>
|
||||
|
||||
<ExecutionInputTable
|
||||
:inputs="inputs"
|
||||
:scenario="scenario"
|
||||
@download-file="downloadFile"
|
||||
/>
|
||||
|
||||
<!-- Chat Button -->
|
||||
<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>
|
||||
</Panel>
|
||||
|
||||
<!-- Workflow Response Panel -->
|
||||
<div v-if="!chat_enabled" class="mt-4">
|
||||
<OldExecutionResponsePanel
|
||||
:scenario="scenario"
|
||||
:exec-scenario="exec_scenario"
|
||||
:scenario-output="scenario_output"
|
||||
:execution-id="execution_id"
|
||||
:is-loading="loadingStore.exectuion_loading && loadingStore.getExecIdLoading === execution_id"
|
||||
:file-type="fileType"
|
||||
:file-content="fileContent"
|
||||
@download-file="downloadCodegenieFile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Chat Panel -->
|
||||
<div v-if="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>
|
||||
|
||||
<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%;
|
||||
}
|
||||
</style>
|
||||
@@ -335,143 +335,612 @@ const chatDisabled = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Header Section -->
|
||||
<div class="flex items-center justify-between p-1">
|
||||
<h1>{{ scenario.name }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-1">
|
||||
<h2>{{ scenario.description }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Chat Return Button -->
|
||||
<div v-if="data_loaded && chat_enabled" class="flex mt-6 justify-center">
|
||||
<div class="card flex flex-col gap-4 w-full items-center">
|
||||
<Button label="Return to scenario" @click="chatDisabled" size="large" iconPos="right" icon="pi pi-backward" class="w-auto"></Button>
|
||||
<div class="scenario-exec-container">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div v-if="scenario.name" class="header-section">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<i class="pi pi-bolt" style="font-size: 1.36rem"></i>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">{{ scenario.name }}</h1>
|
||||
<p class="page-subtitle">{{ scenario.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenario Form -->
|
||||
<div v-else class="flex mt-2">
|
||||
<div class="card flex flex-col w-full">
|
||||
<MarkdownViewer :class="['markdown-content', 'ml-[-20px]']" :modelValue="scenario.hint" />
|
||||
<!-- Chat Return Button -->
|
||||
<div v-if="data_loaded && chat_enabled" class="chat-return-section">
|
||||
<div class="return-card">
|
||||
<Button label="Return to Scenario" @click="chatDisabled" size="large" iconPos="left" icon="pi pi-arrow-left" class="return-button" severity="secondary"></Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="scenario.inputs">
|
||||
<div class="grid grid-cols-2 md:grid-cols-1">
|
||||
<div v-for="input in scenario.inputs" :key="input.name">
|
||||
<!-- Single File Upload -->
|
||||
<div v-if="input.type === 'singlefile' || input.type === 'singlefile_acceptall'">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
tooltip-text="Upload one document from the suggested types. Mandatory if you want to execute scenario."
|
||||
:upload-url="uploadUrlPR"
|
||||
:is-multiple="false"
|
||||
:accepted-formats="acceptedFormats"
|
||||
:folder-name="folderName"
|
||||
v-model:uploaded-files="uploadedFiles"
|
||||
@upload="handleFileUpload"
|
||||
@remove="handleFileRemove"
|
||||
/>
|
||||
<!-- Enhanced Scenario Form -->
|
||||
<div v-else class="form-section">
|
||||
<div class="form-card">
|
||||
<!-- Hint Section -->
|
||||
<div v-if="scenario.hint" class="hint-section">
|
||||
<div class="hint-header">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<span>Scenario Information</span>
|
||||
</div>
|
||||
<MarkdownViewer class="hint-content" :modelValue="scenario.hint" />
|
||||
</div>
|
||||
|
||||
<!-- Inputs Section -->
|
||||
<template v-if="scenario.inputs">
|
||||
<div class="inputs-section">
|
||||
<div class="inputs-grid">
|
||||
<div v-for="input in scenario.inputs" :key="input.name" class="input-item">
|
||||
<!-- Single File Upload -->
|
||||
<div v-if="input.type === 'singlefile' || input.type === 'singlefile_acceptall'" class="input-group">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
tooltip-text="Upload one document from the suggested types. Mandatory if you want to execute scenario."
|
||||
:upload-url="uploadUrlPR"
|
||||
:is-multiple="false"
|
||||
:accepted-formats="acceptedFormats"
|
||||
:folder-name="folderName"
|
||||
v-model:uploaded-files="uploadedFiles"
|
||||
@upload="handleFileUpload"
|
||||
@remove="handleFileRemove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Multi File Upload -->
|
||||
<div v-else-if="input.type === 'multifile'" class="input-group">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
tooltip-text="Upload others documents of .docx, .msg, .text type. Optional."
|
||||
:upload-url="uploadUrlOther"
|
||||
:is-multiple="true"
|
||||
accepted-formats=".msg,.txt,.docx"
|
||||
:folder-name="folderName"
|
||||
v-model:uploaded-files="uploadedFiles"
|
||||
@upload="handleFileUpload"
|
||||
@remove="handleFileRemove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Multi Select with Dynamic Picker -->
|
||||
<div v-else-if="input.type === 'multiselect'" class="input-group">
|
||||
<DynamicPicker
|
||||
v-model="formData[input.name]"
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
:data-source="input.dataSource || 'videoGroups'"
|
||||
:options="getOptionsForInput(input)"
|
||||
:disabled="loadingStore.exectuion_loading"
|
||||
:loading="loadingOptionsFor[input.dataSource] || false"
|
||||
:show-status="input.dataSource === 'ksDocuments'"
|
||||
no-margin
|
||||
@change="onDynamicPickerChange(input.name, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Other Input Types -->
|
||||
<div v-else class="input-group">
|
||||
<label :for="input.name" class="input-label">
|
||||
{{ input.label }}
|
||||
</label>
|
||||
<div class="input-wrapper">
|
||||
<component :is="getInputComponent(input.type)" :id="input.name" v-model="formData[input.name]" :options="input.options" class="full-width-input" :disabled="loadingStore.exectuion_loading" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi File Upload -->
|
||||
<div v-else-if="input.type === 'multifile'">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
tooltip-text="Upload others documents of .docx, .msg, .text type. Optional."
|
||||
:upload-url="uploadUrlOther"
|
||||
:is-multiple="true"
|
||||
accepted-formats=".msg,.txt,.docx"
|
||||
:folder-name="folderName"
|
||||
v-model:uploaded-files="uploadedFiles"
|
||||
@upload="handleFileUpload"
|
||||
@remove="handleFileRemove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Multi Select with Dynamic Picker -->
|
||||
<div v-else-if="input.type === 'multiselect'" class="mt-4">
|
||||
<DynamicPicker
|
||||
v-model="formData[input.name]"
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
:data-source="input.dataSource || 'videoGroups'"
|
||||
:options="getOptionsForInput(input)"
|
||||
:disabled="loadingStore.exectuion_loading"
|
||||
:loading="loadingOptionsFor[input.dataSource] || false"
|
||||
:show-status="input.dataSource === 'ksDocuments'"
|
||||
no-margin
|
||||
@change="onDynamicPickerChange(input.name, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Other Input Types -->
|
||||
<div v-else>
|
||||
<label :for="input.name"
|
||||
><b>{{ input.label }}</b></label
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
<component :is="getInputComponent(input.type)" :id="input.name" v-model="formData[input.name]" :options="input.options" class="full-width-input" :disabled="loadingStore.exectuion_loading" />
|
||||
<!-- Enhanced Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<div v-if="data_loaded && scenario.chatEnabled && !chat_enabled" class="button-group">
|
||||
<Button
|
||||
:disabled="loadingStore.exectuion_loading || !isInputFilled"
|
||||
label="Execute Scenario"
|
||||
@click="execScenario"
|
||||
size="large"
|
||||
iconPos="right"
|
||||
icon="pi pi-play-circle"
|
||||
class="execute-button"
|
||||
severity="success"
|
||||
></Button>
|
||||
<Button label="Open Chat" @click="chatEnabled" size="large" iconPos="right" icon="pi pi-comments" severity="help"></Button>
|
||||
</div>
|
||||
<div v-else class="button-group">
|
||||
<Button
|
||||
:disabled="loadingStore.exectuion_loading || !isInputFilled"
|
||||
label="Execute Scenario"
|
||||
@click="execScenario"
|
||||
size="large"
|
||||
iconPos="right"
|
||||
icon="pi pi-play-circle"
|
||||
class="execute-button"
|
||||
severity="success"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div v-if="data_loaded && scenario.chatEnabled" class="flex justify-center">
|
||||
<div v-if="!chat_enabled" class="flex gap-4 mt-6">
|
||||
<Button :disabled="loadingStore.exectuion_loading || !isInputFilled" label="Execute" @click="execScenario" size="large" iconPos="right" icon="pi pi-cog"></Button>
|
||||
<Button label="Open Chat" @click="chatEnabled" size="large" iconPos="right" icon="pi pi-comments"></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-center mt-6">
|
||||
<Button :disabled="loadingStore.exectuion_loading || !isInputFilled" label="Execute" @click="execScenario" size="large" iconPos="right" icon="pi pi-cog"></Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execution Timer -->
|
||||
<ExecutionTimer :is-loading="loading_data" :message="scenario_response_message" />
|
||||
|
||||
<!-- Workflow Response Panel -->
|
||||
<div v-if="data_loaded && !chat_enabled">
|
||||
<WorkflowResponsePanel ref="responsePanel" :scenario="scenario" :scenario-output="scenario_output" :exec-id="exec_id" :error-message="error_message" :errored-execution="errored_execution" />
|
||||
</div>
|
||||
|
||||
<!-- Chat Panel -->
|
||||
<div v-if="data_loaded && 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="exec_id" />
|
||||
</template>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<!-- Execution Timer -->
|
||||
<div class="timer-section">
|
||||
<ExecutionTimer :is-loading="loading_data" :message="scenario_response_message" />
|
||||
</div>
|
||||
|
||||
<!-- Workflow Response Panel -->
|
||||
<div v-if="data_loaded && !chat_enabled" class="response-section">
|
||||
<WorkflowResponsePanel ref="responsePanel" :scenario="scenario" :scenario-output="scenario_output" :exec-id="exec_id" :error-message="error_message" :errored-execution="errored_execution" />
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Chat Panel -->
|
||||
<div v-if="data_loaded && chat_enabled" class="chat-section">
|
||||
<Panel class="chat-panel">
|
||||
<template #header>
|
||||
<div class="chat-header">
|
||||
<i class="pi pi-comments chat-icon"></i>
|
||||
<span class="chat-title">Chat with WizardAI</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chat-content">
|
||||
<ChatClient :scenarioExecutionId="exec_id" />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-wrapper {
|
||||
/* Container */
|
||||
.scenario-exec-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced Header Section */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInDown 0.5s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-top: 10px;
|
||||
gap: 1.2rem;
|
||||
padding: 1.36rem;
|
||||
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.full-width-input {
|
||||
width: 100%;
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Chat Return Section */
|
||||
.chat-return-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.return-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.return-card :deep(button) {
|
||||
min-width: 250px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.return-button {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* Form Section */
|
||||
.form-section {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hint Section */
|
||||
.hint-section {
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, rgba(161, 0, 255, 0.1) 0%, rgba(123, 0, 204, 0.1) 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
}
|
||||
|
||||
.hint-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #a100ff;
|
||||
}
|
||||
|
||||
.hint-header i {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.hint-content {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
background-color: #f5f5f5;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.hint-content :deep(*) {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.hint-content :deep(.markdown-content) {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.hint-content :deep(p),
|
||||
.hint-content :deep(div),
|
||||
.hint-content :deep(span),
|
||||
.hint-content :deep(pre),
|
||||
.hint-content :deep(code) {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Inputs Section */
|
||||
.inputs-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.inputs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inputs-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.input-item {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(161, 0, 255, 0.15);
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* File Upload Specific Styling */
|
||||
.input-item :deep(.p-fileupload) {
|
||||
border: 2px dashed #cbd5e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-item :deep(.p-fileupload:hover) {
|
||||
border-color: #a100ff;
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.input-item :deep(.p-fileupload-buttonbar) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.input-item :deep(.p-fileupload-content) {
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Upload area styling */
|
||||
.input-item :deep(.p-fileupload-choose) {
|
||||
background: #a100ff;
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.input-item :deep(.p-fileupload-choose:hover) {
|
||||
background: #5568d3;
|
||||
border-color: #5568d3;
|
||||
}
|
||||
|
||||
/* DynamicPicker Component Styling */
|
||||
.input-item :deep(.dynamic-picker-label) {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-item :deep(.dynamic-picker-label::before) {
|
||||
content: '•';
|
||||
color: #a100ff;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #2d3748;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-label::before {
|
||||
content: '•';
|
||||
color: #a100ff;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.full-width-input {
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.full-width-input:focus {
|
||||
transform: scale(1.01);
|
||||
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Style for all input fields within input-group */
|
||||
.input-group :deep(.p-inputtext),
|
||||
.input-group :deep(.p-textarea),
|
||||
.input-group :deep(.p-dropdown),
|
||||
.input-group :deep(.p-multiselect) {
|
||||
background: white;
|
||||
border: 1px solid #cbd5e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-group :deep(.p-inputtext:hover),
|
||||
.input-group :deep(.p-textarea:hover),
|
||||
.input-group :deep(.p-dropdown:hover),
|
||||
.input-group :deep(.p-multiselect:hover) {
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.input-group :deep(.p-inputtext:focus),
|
||||
.input-group :deep(.p-textarea:focus),
|
||||
.input-group :deep(.p-dropdown:focus),
|
||||
.input-group :deep(.p-multiselect:focus) {
|
||||
border-color: #a100ff;
|
||||
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group :deep(button) {
|
||||
min-width: 200px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.execute-button {
|
||||
min-width: 200px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Timer Section */
|
||||
.timer-section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Response Section */
|
||||
.response-section {
|
||||
margin-top: 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.response-section :deep(.card),
|
||||
.response-section :deep(.p-panel) {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.response-section :deep(.p-panel-header) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
/* Chat Section */
|
||||
.chat-section {
|
||||
margin-top: 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.chat-panel :deep(.p-panel-header) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #a100ff;
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.scenario-exec-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.scenario-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.inputs-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.full-width-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
364
src/views/pages/ScenarioExecHistory.vue
Normal file
364
src/views/pages/ScenarioExecHistory.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<script setup>
|
||||
import ExecutionChatSection from '@/components/ExecutionChatSection.vue';
|
||||
import ExecutionInputSection from '@/components/ExecutionInputSection.vue';
|
||||
import ExecutionResponseSection from '@/components/ExecutionResponseSection.vue';
|
||||
import { useChatToggle } from '@/composables/useChatToggle';
|
||||
import { useFileDownload } from '@/composables/useFileDownload';
|
||||
import { useFileProcessing } from '@/composables/useFileProcessing';
|
||||
import { ScenarioService } from '@/service/ScenarioService.js';
|
||||
import { LoadingStore } from '@/stores/LoadingStore.js';
|
||||
import { ScenarioExecutionStore } from '@/stores/ScenarioExecutionStore.js';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
// ============= Stores and Services =============
|
||||
const loadingStore = LoadingStore();
|
||||
const scenarioExecutionStore = ScenarioExecutionStore();
|
||||
|
||||
// ============= Composables =============
|
||||
const { extractFiles, showFileContent } = useFileProcessing();
|
||||
const { downloadFile, downloadCodegenieFile } = useFileDownload();
|
||||
const { chatEnabled, enableChat, disableChat } = useChatToggle();
|
||||
|
||||
// ============= Reactive State =============
|
||||
const scenario = ref({});
|
||||
const exec_scenario = ref({});
|
||||
const scenario_output = ref(null);
|
||||
const inputs = ref(null);
|
||||
const execution_id = ref(null);
|
||||
const rating = ref(null);
|
||||
const loading = ref(false);
|
||||
const data_loaded = ref(false);
|
||||
|
||||
// ============= Lifecycle Hooks =============
|
||||
onMounted(() => {
|
||||
const execution = scenarioExecutionStore.getSelectedExecScenario;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// ============= Data Fetching Methods =============
|
||||
const retrieveScenarioExec = async (id) => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await ScenarioService.getScenarioExecutionById(id);
|
||||
|
||||
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;
|
||||
inputs.value = response.data.scenarioExecutionInput.inputs;
|
||||
|
||||
// Handle file processing for MultiFileUpload scenarios
|
||||
await handleFileProcessing();
|
||||
} catch (error) {
|
||||
console.error('Error retrieving scenario execution:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= File Processing Methods =============
|
||||
const handleFileProcessing = async () => {
|
||||
if (inputs.value['MultiFileUpload'] && scenario.value.steps?.[0]?.attributes?.['codegenie_output_type']) {
|
||||
try {
|
||||
// Extract input files
|
||||
await extractFiles(inputs.value['MultiFileUpload'], 'input');
|
||||
|
||||
const outputType = scenario.value.steps[0].attributes['codegenie_output_type'];
|
||||
|
||||
// Show file content for MARKDOWN or JSON types
|
||||
if (outputType === 'MARKDOWN' || outputType === 'JSON') {
|
||||
showFileContent(scenario_output.value, outputType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing files:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============= Download Methods =============
|
||||
const handleDownloadFile = async (filePath) => {
|
||||
await downloadFile(filePath, execution_id.value);
|
||||
};
|
||||
|
||||
const handleDownloadCodegenieFile = (base64String) => {
|
||||
downloadCodegenieFile(base64String, execution_id.value);
|
||||
};
|
||||
|
||||
// ============= Rating Methods =============
|
||||
const handleRatingUpdate = (newRating) => {
|
||||
rating.value = newRating;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scenario-history-container">
|
||||
<!-- Loading Spinner -->
|
||||
<div v-if="loading" class="loading-section">
|
||||
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="3" fill="transparent" />
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div v-if="data_loaded">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div class="header-section">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<i class="pi pi-history" style="font-size: 1.36rem"></i>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">{{ scenario.name }}</h1>
|
||||
<p class="page-subtitle">Execution History</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execution Input Section -->
|
||||
<div class="input-section">
|
||||
<ExecutionInputSection :execution-id="execution_id" :inputs="inputs" :scenario="scenario" :exec-scenario="exec_scenario" :rating="rating" :show-rating="true" @download-file="handleDownloadFile" @rating-updated="handleRatingUpdate" />
|
||||
</div>
|
||||
|
||||
<!-- Chat Toggle Button -->
|
||||
<div v-if="scenario.chatEnabled && exec_scenario.latestStepStatus !== 'ERROR'" class="chat-toggle-section">
|
||||
<div class="toggle-card">
|
||||
<div v-if="!chatEnabled" class="button-group">
|
||||
<Button label="Open Chat" @click="enableChat" size="large" iconPos="right" icon="pi pi-comments" severity="help" />
|
||||
</div>
|
||||
<div v-else class="button-group">
|
||||
<Button label="Close Chat" @click="disableChat" size="large" iconPos="left" icon="pi pi-times" severity="help" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflow Response Section -->
|
||||
<div v-if="!chatEnabled" class="response-section">
|
||||
<ExecutionResponseSection
|
||||
:scenario="scenario"
|
||||
:exec-scenario="exec_scenario"
|
||||
:scenario-output="scenario_output"
|
||||
:execution-id="execution_id"
|
||||
:is-loading="loadingStore.exectuion_loading && loadingStore.getExecIdLoading === execution_id"
|
||||
mode="history"
|
||||
@download-file="handleDownloadCodegenieFile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Chat Section -->
|
||||
<div v-if="chatEnabled" class="chat-section">
|
||||
<ExecutionChatSection :execution-id="execution_id" :scenario-name="scenario.name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Container */
|
||||
.scenario-history-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Loading Section */
|
||||
.loading-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Enhanced Header Section */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInDown 0.5s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 1.36rem;
|
||||
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Input Section */
|
||||
.input-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.input-section :deep(.card),
|
||||
.input-section :deep(.p-panel) {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-section :deep(.p-panel-header) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
/* Chat Toggle Section */
|
||||
.chat-toggle-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.toggle-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group :deep(button) {
|
||||
min-width: 200px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Response Section */
|
||||
.response-section {
|
||||
margin-top: 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.response-section :deep(.card),
|
||||
.response-section :deep(.p-panel) {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.response-section :deep(.p-panel-header) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
/* Chat Section */
|
||||
.chat-section {
|
||||
margin-top: 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.scenario-history-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.scenario-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,173 +1,4 @@
|
||||
<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 >
|
||||
|
||||
<h2 class="text-xl font-bold mt-6">Executions List</h2>
|
||||
|
||||
<DataTable v-model:filters="filters" v-model:expandedRows="expandedRows" @rowExpand="onRowExpand"
|
||||
@rowCollapse="onRowCollapse" :value="scenario_execution_store.scenariosExecution"
|
||||
:loading="loading_data"
|
||||
:paginator="true"
|
||||
:lazy="true"
|
||||
:rows="scenario_execution_store.getPageSize"
|
||||
:first="scenario_execution_store.getCurrentPage * scenario_execution_store.getPageSize"
|
||||
:totalRecords="scenario_execution_store.getTotalRecords"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} records"
|
||||
:rowsPerPageOptions="[10, 15, 20, 50, 100]" dataKey="id" :rowHover="true" rowGroupMode="subheader"
|
||||
:sortOrder="1" filterDisplay="menu"
|
||||
tableStyle="min-width: 70rem"
|
||||
@page="onPage"
|
||||
@sort="onSort"
|
||||
removableSort>
|
||||
|
||||
|
||||
<template #header>
|
||||
<div class="flex justify-end">
|
||||
|
||||
<IconField>
|
||||
<Button label="Clear Filters" @click="clearFilters" class="mr-2" />
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['_id'].constraints[0].value" placeholder="ID" />
|
||||
<Button label="Apply" @click="fetchData(0, 10)" />
|
||||
|
||||
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<Column field="scenario.name" header="Scenario Name" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false"
|
||||
style="min-width: 12rem" >
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.scenario?.name }}
|
||||
<i
|
||||
class="pi pi-info-circle text-violet-600 cursor-pointer"
|
||||
v-tooltip="slotProps.data?.scenario?.description || 'No description available'"
|
||||
></i>
|
||||
<!-- controllare il tooltip -->
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search by ScenarioName" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
|
||||
</Column>
|
||||
|
||||
<Column field="execSharedMap.user_input.selected_application" header="Application Input" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false"
|
||||
style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.execSharedMap?.user_input?.selected_application }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text"
|
||||
placeholder="Search by Application" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
|
||||
</template>
|
||||
</Column>
|
||||
<!-- <Column field="startDate"
|
||||
filterField="startDate" header="Start Date" sortable
|
||||
style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Calendar
|
||||
v-model="filterModel.value"
|
||||
@input="(value) => {
|
||||
filterModel.value = new Date(value); // Converte in oggetto Date
|
||||
filterCallback();
|
||||
}"
|
||||
dateFormat="yy-mm-dd"
|
||||
placeholder="Filter by Date"
|
||||
/>
|
||||
</template>
|
||||
</Column> -->
|
||||
<Column field="startDate" header="Start Date" filterField="startDate" dataType="date" style="min-width: 10rem" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
|
||||
</template>
|
||||
<!-- <template #filter="{ filterModel, filterCallback }">
|
||||
<DatePicker v-model="filterModel.value" dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template> -->
|
||||
</Column>
|
||||
|
||||
<Column field="scenario.aiModel.model" header="Model AI"
|
||||
style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.scenario?.aiModel?.model }}
|
||||
<i
|
||||
class="pi pi-info-circle text-violet-600 cursor-pointer"
|
||||
v-tooltip="'Provider: ' + slotProps.data?.scenario?.aiModel?.apiProvider + ' Token used: ' + slotProps.data?.usedTokens || 'No description available'"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text"
|
||||
placeholder="Search by Model" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
|
||||
</template> -->
|
||||
</Column>
|
||||
<Column field="executedByUsername" header="Executed By" sortable
|
||||
style="min-width: 12rem" :showApplyButton="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.executedByUsername || 'N/A' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text"
|
||||
placeholder="Search by Username" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="rating" header="Rating" sortable :showApplyButton="false" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
|
||||
<template #body="slotProps">
|
||||
<Rating :modelValue="slotProps.data.rating" :stars="5" :readonly="true" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search (1,2,3,4,5)" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="id" :style="{ position: 'sticky', right: '0', zIndex: '1', background: '#f3f3f3'}">
|
||||
<template #body="slotProps">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<Button label="View" @click="goToScenarioExec(slotProps.data)" class="mt-0 ml-0" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #empty>
|
||||
<tr>
|
||||
<td :colspan="9" class="text-center">No execution found</td>
|
||||
</tr>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<div v-if="loading_data" class="flex justify-center">
|
||||
<ProgressSpinner style="width: 30px; height: 30px; margin: 30px" strokeWidth="6" fill="transparent"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { FilterMatchMode, FilterOperator } from '@primevue/core/api';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import moment from 'moment';
|
||||
@@ -191,7 +22,7 @@ const formData = ref({});
|
||||
const exec_id = ref(null);
|
||||
const exec_scenario = ref({});
|
||||
const debug_modal = ref(false);
|
||||
const execution_id = ref("");
|
||||
const execution_id = ref('');
|
||||
const listScenarios = ref([]);
|
||||
const scenario_execution_store = ScenarioExecutionStore();
|
||||
const toast = useToast();
|
||||
@@ -202,43 +33,40 @@ const actualPageSize = ref(10);
|
||||
const sortField = ref(null);
|
||||
const sortOrder = ref(null);
|
||||
|
||||
|
||||
|
||||
|
||||
const filters = ref({
|
||||
'_id': { operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
|
||||
},
|
||||
_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'scenario.name': {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
|
||||
},
|
||||
'execSharedMap.user_input.selected_application': {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{
|
||||
value: userPrefStore.getSelApp?.fe_name || null, matchMode: FilterMatchMode.CONTAINS
|
||||
}]
|
||||
},
|
||||
'execSharedMap.user_input.selected_application': {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [
|
||||
{
|
||||
value: userPrefStore.getSelApp?.fe_name || null,
|
||||
matchMode: FilterMatchMode.CONTAINS
|
||||
}
|
||||
]
|
||||
},
|
||||
'scenario.aiModel.model': {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
|
||||
},
|
||||
},
|
||||
|
||||
'executedByUsername': {
|
||||
executedByUsername: {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
|
||||
},
|
||||
|
||||
'startDate': {
|
||||
},
|
||||
|
||||
startDate: {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }]
|
||||
},
|
||||
},
|
||||
|
||||
'rating': {
|
||||
rating: {
|
||||
operator: FilterOperator.AND,
|
||||
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -253,16 +81,13 @@ watch(() => route.params.name, updateFilters);
|
||||
// fetchData(Math.floor(first.value / scenario_execution_store.getPageSize), scenario_execution_store.getPageSize, filters.value);
|
||||
// });
|
||||
|
||||
|
||||
|
||||
function updateFilters() {
|
||||
const selectedScenario = userPrefStore.getSelScenario;
|
||||
|
||||
if (selectedScenario && route.params.name!=='all') {
|
||||
if (selectedScenario && route.params.name !== 'all') {
|
||||
console.log('selectedScenario: im in');
|
||||
filters.value['scenario.name'].constraints[0].value = selectedScenario;
|
||||
|
||||
}else{
|
||||
} else {
|
||||
filters.value['scenario.name'].constraints[0].value = null;
|
||||
}
|
||||
fetchData(0, 10);
|
||||
@@ -275,11 +100,11 @@ function fetchDataWithFilters(filterCallback) {
|
||||
|
||||
function onSort(event) {
|
||||
console.log('Sorting event:', event);
|
||||
|
||||
|
||||
sortField.value = event.sortField;
|
||||
sortOrder.value = event.sortOrder;
|
||||
|
||||
fetchData(0,actualPageSize.value);
|
||||
fetchData(0, actualPageSize.value);
|
||||
}
|
||||
|
||||
const fetchData = async (page, size) => {
|
||||
@@ -298,12 +123,12 @@ const fetchData = async (page, size) => {
|
||||
|
||||
function clearFilters() {
|
||||
filters.value = {
|
||||
'_id': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'scenario.name': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'execSharedMap.user_input.selected_application': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'scenario.aiModel.model': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'executedByUsername': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
'startDate': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }
|
||||
executedByUsername: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
|
||||
startDate: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }
|
||||
};
|
||||
fetchData(0, actualPageSize.value);
|
||||
}
|
||||
@@ -311,36 +136,545 @@ function clearFilters() {
|
||||
const goToScenarioExec = (execScenarioItem) => {
|
||||
console.log(execScenarioItem);
|
||||
|
||||
router.push({ name: 'scenario-exec-history', query: { id:execScenarioItem.id } });
|
||||
|
||||
router.push({ name: 'scenario-exec-history', query: { id: execScenarioItem.id } });
|
||||
|
||||
// scenario_execution_store.setSelectedExecScenario(execScenarioItem).then(() => {
|
||||
// router.push({ name: 'scenario-exec-history', query: { id:execScenarioItem.id } });
|
||||
// });
|
||||
};
|
||||
|
||||
function onPage(event) {
|
||||
function onPage(event) {
|
||||
actualPage.value = event.page;
|
||||
actualPageSize.value = event.rows;
|
||||
fetchData(event.page, event.rows);
|
||||
console.log('event onpage:', event);
|
||||
//updateFilters();
|
||||
}
|
||||
}
|
||||
// function onPage(event) {
|
||||
// first.value = event.first; // Imposta la pagina corrente
|
||||
// fetchData(Math.floor(first.value / scenario_execution_store.getPageSize), scenario_execution_store.getPageSize);
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="exec-list-container">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div class="header-section">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<i class="pi pi-list" style="font-size: 1.36rem"></i>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Scenario Executions</h1>
|
||||
<p class="page-subtitle">View and manage all scenario execution history</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-section">
|
||||
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="3" fill="transparent" />
|
||||
</div>
|
||||
|
||||
<div v-else class="table-section">
|
||||
<DataTable
|
||||
v-model:filters="filters"
|
||||
v-model:expandedRows="expandedRows"
|
||||
@rowExpand="onRowExpand"
|
||||
@rowCollapse="onRowCollapse"
|
||||
:value="scenario_execution_store.scenariosExecution"
|
||||
:loading="loading_data"
|
||||
:paginator="true"
|
||||
:lazy="true"
|
||||
:rows="scenario_execution_store.getPageSize"
|
||||
:first="scenario_execution_store.getCurrentPage * scenario_execution_store.getPageSize"
|
||||
:totalRecords="scenario_execution_store.getTotalRecords"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} records"
|
||||
:rowsPerPageOptions="[10, 15, 20, 50, 100]"
|
||||
dataKey="id"
|
||||
:rowHover="true"
|
||||
rowGroupMode="subheader"
|
||||
:sortOrder="1"
|
||||
filterDisplay="menu"
|
||||
tableStyle="min-width: 70rem"
|
||||
@page="onPage"
|
||||
@sort="onSort"
|
||||
removableSort
|
||||
class="enhanced-table"
|
||||
>
|
||||
<template #header>
|
||||
<div class="table-header">
|
||||
<div class="table-header-content">
|
||||
<i class="pi pi-filter"></i>
|
||||
<h3 class="table-title">Filter Executions</h3>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<IconField class="search-field">
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['_id'].constraints[0].value" placeholder="Search by ID" class="search-input" />
|
||||
</IconField>
|
||||
<Button label="Clear" @click="clearFilters" icon="pi pi-times" severity="secondary" outlined class="action-button" />
|
||||
<Button label="Apply" @click="fetchData(0, 10)" icon="pi pi-check" severity="success" class="action-button" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="scenario.name" header="Scenario Name" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false" style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.scenario?.name }}
|
||||
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="slotProps.data?.scenario?.description || 'No description available'"></i>
|
||||
<!-- controllare il tooltip -->
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search by ScenarioName" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="execSharedMap.user_input.selected_application" header="Application Input" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false" style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.execSharedMap?.user_input?.selected_application }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search by Application" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
</Column>
|
||||
<!-- <Column field="startDate"
|
||||
filterField="startDate" header="Start Date" sortable
|
||||
style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Calendar
|
||||
v-model="filterModel.value"
|
||||
@input="(value) => {
|
||||
filterModel.value = new Date(value); // Converte in oggetto Date
|
||||
filterCallback();
|
||||
}"
|
||||
dateFormat="yy-mm-dd"
|
||||
placeholder="Filter by Date"
|
||||
/>
|
||||
</template>
|
||||
</Column> -->
|
||||
<Column field="startDate" header="Start Date" filterField="startDate" dataType="date" style="min-width: 10rem" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
|
||||
</template>
|
||||
<!-- <template #filter="{ filterModel, filterCallback }">
|
||||
<DatePicker v-model="filterModel.value" dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template> -->
|
||||
</Column>
|
||||
|
||||
<Column field="scenario.aiModel.model" header="Model AI" style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.scenario?.aiModel?.model }}
|
||||
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="'Provider: ' + slotProps.data?.scenario?.aiModel?.apiProvider + ' Token used: ' + slotProps.data?.usedTokens || 'No description available'"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text"
|
||||
placeholder="Search by Model" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
|
||||
</template> -->
|
||||
</Column>
|
||||
<Column field="executedByUsername" header="Executed By" sortable style="min-width: 12rem" :showApplyButton="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
|
||||
<template #body="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ slotProps.data.executedByUsername || 'N/A' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search by Username" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="rating" header="Rating" sortable :showApplyButton="false" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
|
||||
<template #body="slotProps">
|
||||
<Rating :modelValue="slotProps.data.rating" :stars="5" :readonly="true" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" placeholder="Search (1,2,3,4,5)" />
|
||||
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="id" :style="{ position: 'sticky', right: '0', zIndex: '1', background: '#f3f3f3' }">
|
||||
<template #body="slotProps">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<Button label="View" @click="goToScenarioExec(slotProps.data)" class="mt-0 ml-0" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<i class="pi pi-inbox empty-icon"></i>
|
||||
<p class="empty-text">No executions found</p>
|
||||
<p class="empty-subtext">Try adjusting your filters or execute a new scenario</p>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<div v-if="loading_data" class="table-loading">
|
||||
<ProgressSpinner style="width: 30px; height: 30px" strokeWidth="6" fill="transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Container */
|
||||
.exec-list-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced Header Section */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInDown 0.5s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 1.36rem;
|
||||
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Loading Section */
|
||||
.loading-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Table Section */
|
||||
.table-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.enhanced-table {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-datatable-header) {
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.table-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #a100ff;
|
||||
}
|
||||
|
||||
.table-header-content i {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #a100ff;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.search-input :deep(.p-inputtext) {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #cbd5e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input :deep(.p-inputtext:hover) {
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.search-input :deep(.p-inputtext:focus) {
|
||||
border-color: #a100ff;
|
||||
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
min-width: 120px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button:deep(button) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-button:not(:disabled):hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Table Styling */
|
||||
.enhanced-table :deep(.p-datatable-thead > tr > th) {
|
||||
background: #f8f9fa;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-datatable-tbody > tr) {
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-datatable-tbody > tr:hover) {
|
||||
background: #f7fafc;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-datatable-tbody > tr > td) {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Filter Column Templates */
|
||||
.enhanced-table :deep(.p-column-filter-overlay) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-column-filter-overlay .p-inputtext) {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #cbd5e0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Info Icons */
|
||||
.enhanced-table :deep(.pi-info-circle) {
|
||||
color: #a100ff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.pi-info-circle:hover) {
|
||||
transform: scale(1.2);
|
||||
color: #5568d3;
|
||||
}
|
||||
|
||||
/* Rating Component */
|
||||
.enhanced-table :deep(.p-rating) {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-rating .p-rating-icon) {
|
||||
color: #a100ff;
|
||||
}
|
||||
|
||||
/* View Button */
|
||||
.enhanced-table :deep(.p-button) {
|
||||
min-width: 100px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.enhanced-table :deep(.p-button:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(161, 0, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
/* Table Loading */
|
||||
.table-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Sticky Column Styling */
|
||||
.enhanced-table :deep(td[style*='position: sticky']) {
|
||||
background: linear-gradient(to left, #ffffff 0%, #ffffff 90%, transparent 100%) !important;
|
||||
box-shadow: -4px 0 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.exec-list-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.scenario-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy Styles for Compatibility */
|
||||
.input-container {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@@ -348,14 +682,11 @@ const goToScenarioExec = (execScenarioItem) => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.editor ol {
|
||||
list-style-type: decimal !important;
|
||||
list-style-type: decimal !important;
|
||||
}
|
||||
|
||||
.editor ul {
|
||||
list-style-type: disc !important;
|
||||
list-style-type: disc !important;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,139 +1,226 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Available Scenarios</h1>
|
||||
</div>
|
||||
<div >
|
||||
<DataView :value="scenario_store.filteredScenarios" :layout="layout" paginator :rows="8">
|
||||
<template #header>
|
||||
<div class="header-container">
|
||||
<div class="search-bar">
|
||||
<i class="pi pi-search search-icon"></i>
|
||||
<InputText
|
||||
class="search-input"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
v-model="scenario_store.filterString"
|
||||
size="medium"
|
||||
variant="filled"
|
||||
style="border: 1px solid #a100ff"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card flex justify-center">
|
||||
<SelectButton v-model="scenario_store.typeFilter" :options="scenarioTypeOp" optionLabel="name" />
|
||||
</div>
|
||||
|
||||
<SelectButton v-model="layout" :options="options" :allowEmpty="false" class="layout-switch">
|
||||
<template #option="{ option }">
|
||||
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-table']" />
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #list="slotProps">
|
||||
<div class="flex flex-col space-y-4 mt-2">
|
||||
<div v-for="(item, index) in slotProps.items" :key="index">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center p-6 gap-4 bg-white dark:bg-gray-800 rounded-lg shadow-md"
|
||||
:class="{ 'border-t border-gray-200 dark:border-gray-700': index !== 0 }">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</h3>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-2">{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="mt-auto flex justify-end">
|
||||
|
||||
<Button @click="executeScenario(item.id)" label="Load" class="flex-auto md:flex-initial text-white">
|
||||
<ChevronRightIcon class="w-5 h-10 text-white transition-transform transform hover:translate-x-1"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #grid="slotProps">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
<div v-for="(item, index) in slotProps.items" :key="index" class="p-2">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md flex flex-col h-full">
|
||||
<div class="p-4 flex flex-col flex-grow">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</h3>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-2">{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="p-2 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center w-full">
|
||||
<div v-if="item.visible==='DRAFT'" class="text-xs font-semibold text-white bg-purple-400 px-2 py-1 inline-flex items-center justify-center w-auto">
|
||||
{{ item.visible }}
|
||||
</div>
|
||||
<Button @click="executeScenario(item.id)" size="small" label="Load" class="ml-auto flex-initial text-white">
|
||||
<ChevronRightIcon class="w-6 h-5 text-white transition-transform transform hover:translate-x-1"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DataView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from '@heroicons/vue/24/solid';
|
||||
import DataView from 'primevue/dataview';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ScenarioStore } from '../../stores/ScenarioStore.js';
|
||||
import { UserPrefStore } from '../../stores/UserPrefStore.js';
|
||||
|
||||
const router = useRouter()
|
||||
const layout = ref('grid');
|
||||
const options = ref(['list', 'grid']);
|
||||
const router = useRouter();
|
||||
const layout = ref('grid');
|
||||
const options = ref(['list', 'grid']);
|
||||
|
||||
const scenario_store = ScenarioStore();
|
||||
const userPrefStore = UserPrefStore();
|
||||
const scenario_store = ScenarioStore();
|
||||
const userPrefStore = UserPrefStore();
|
||||
|
||||
const scenarioTypeOp = ref([
|
||||
{ name: 'All', value: 'all' },
|
||||
//{ name: 'Cross', value: 'cross' },
|
||||
{ name: 'Project', value: 'project' },
|
||||
{ name: 'Application', value: 'application' }
|
||||
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
scenario_store.setFilterString('');
|
||||
userPrefStore.fetchUserData().then(() => {
|
||||
scenario_store.fetchScenarios();
|
||||
if (userPrefStore.getSelApp != null) {
|
||||
scenario_store.fetchApplicationScenarios();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
scenario_store.setFilterString('');
|
||||
userPrefStore.fetchUserData().then(() => {
|
||||
//scenario_store.fetchScenariosCross();
|
||||
scenario_store.fetchScenarios();
|
||||
if(userPrefStore.getSelApp != null){
|
||||
scenario_store.fetchApplicationScenarios();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const executeScenario = (id) => {
|
||||
router.push({ name: 'scenario-exec', params: { id: id } });
|
||||
}
|
||||
|
||||
const executeScenario = (id) => {
|
||||
router.push({ name: 'scenario-exec', params: { id: id } });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scenario-list-container">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div class="header-section">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<i class="pi pi-list" style="font-size: 1.36rem"></i>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Available Scenarios</h1>
|
||||
<p class="page-subtitle">Browse and execute scenarios tailored for your project</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced DataView Section -->
|
||||
<div class="dataview-section">
|
||||
<DataView :value="scenario_store.filteredScenarios" :layout="layout" paginator :rows="8" class="custom-dataview">
|
||||
<template #header>
|
||||
<div class="dataview-header">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-container">
|
||||
<i class="pi pi-search search-icon"></i>
|
||||
<InputText class="search-input" type="search" placeholder="Search scenarios..." v-model="scenario_store.filterString" />
|
||||
</div>
|
||||
|
||||
<!-- Type Filter -->
|
||||
<div class="filter-container">
|
||||
<SelectButton v-model="scenario_store.typeFilter" :options="scenarioTypeOp" optionLabel="name" class="type-filter" />
|
||||
</div>
|
||||
|
||||
<!-- Layout Switch -->
|
||||
<SelectButton v-model="layout" :options="options" :allowEmpty="false" class="layout-switch">
|
||||
<template #option="{ option }">
|
||||
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-th-large']" />
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #list="slotProps">
|
||||
<div class="list-view">
|
||||
<div v-for="(item, index) in slotProps.items" :key="index" class="list-item">
|
||||
<div class="list-item-content">
|
||||
<div class="item-info">
|
||||
<div class="item-header">
|
||||
<i class="pi pi-bolt item-icon"></i>
|
||||
<h3 class="item-title">{{ item.name }}</h3>
|
||||
<span v-if="item.visible === 'DRAFT'" class="draft-badge">DRAFT</span>
|
||||
</div>
|
||||
<p class="item-description">{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<Button @click="executeScenario(item.id)" label="Load Scenario" severity="success" icon="pi pi-arrow-right" iconPos="right" class="load-button"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #grid="slotProps">
|
||||
<div class="grid-view">
|
||||
<div v-for="(item, index) in slotProps.items" :key="index" class="grid-item">
|
||||
<div class="grid-item-card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-title">
|
||||
<i class="pi pi-bolt card-icon"></i>
|
||||
<h3 class="card-title">{{ item.name }}</h3>
|
||||
</div>
|
||||
<span v-if="item.visible === 'DRAFT'" class="draft-badge-small">DRAFT</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="card-description">{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<Button @click="executeScenario(item.id)" label="Load" severity="success" icon="pi pi-arrow-right" iconPos="right" size="small" class="card-button"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DataView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* Container */
|
||||
.scenario-list-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
/* Enhanced Header Section */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInDown 0.5s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 1.36rem;
|
||||
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* DataView Section */
|
||||
.dataview-section {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.custom-dataview {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dataview-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 2px solid #a100ff;
|
||||
}
|
||||
|
||||
/* Search Container */
|
||||
.search-container {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-container:hover {
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.search-container:focus-within {
|
||||
border-color: #a100ff;
|
||||
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color:#334155;
|
||||
margin-right: 5px;
|
||||
color: #a100ff;
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@@ -141,6 +228,8 @@ const scenarioTypeOp = ref([
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
flex: 1;
|
||||
font-size: 0.95rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
@@ -149,4 +238,301 @@ const scenarioTypeOp = ref([
|
||||
outline: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
/* Filter Container */
|
||||
.filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.type-filter :deep(.p-button) {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.type-filter :deep(.p-button:hover) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Layout Switch */
|
||||
.layout-switch :deep(.p-button) {
|
||||
border-radius: 8px;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-switch :deep(.p-button i) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* List View */
|
||||
.list-view {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(161, 0, 255, 0.15);
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
color: #a100ff;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.draft-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #a100ff;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.load-button :deep(.p-button) {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Grid View */
|
||||
.grid-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.grid-item-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-item-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(161, 0, 255, 0.2);
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.25rem;
|
||||
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.card-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
color: #a100ff;
|
||||
font-size: 1.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.draft-badge-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #a100ff;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.card-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.scenario-list-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dataview-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.load-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* DataView Override Styles */
|
||||
.custom-dataview :deep(.p-dataview-content) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-dataview :deep(.p-paginator) {
|
||||
background: white;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.custom-dataview :deep(.p-paginator .p-paginator-pages .p-paginator-page.p-highlight) {
|
||||
background: #a100ff;
|
||||
border-color: #a100ff;
|
||||
}
|
||||
|
||||
.custom-dataview :deep(.p-paginator .p-paginator-pages .p-paginator-page:hover) {
|
||||
background: #a100ff15;
|
||||
border-color: #a100ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,150 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between p-1">
|
||||
<h1 class="flex items-center">
|
||||
<i class="pi pi-comments mr-2"></i>
|
||||
<span>Chat with WizardAI</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-1">
|
||||
<h2>
|
||||
<span>
|
||||
Contextualized on
|
||||
</span><br/>
|
||||
<span>
|
||||
Project: <strong>{{ userPrefStore.user.selectedProject.fe_name }}</strong>
|
||||
</span><br/>
|
||||
<span v-if="userPrefStore.user.selectedApplication">
|
||||
Application: <strong>{{ userPrefStore.user.selectedApplication.fe_name}}</strong>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="card">
|
||||
<ChatClient />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ChatClient from '@/components/ChatClient.vue';
|
||||
import { UserPrefStore } from '@/stores/UserPrefStore.js';
|
||||
import { onMounted, computed, watch, ref} from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const userPrefStore = UserPrefStore();
|
||||
|
||||
onMounted(() => {
|
||||
console.log('userPrefStore', userPrefStore);
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style >
|
||||
<template>
|
||||
<div class="chat-page-container">
|
||||
<!-- Enhanced Header Section -->
|
||||
<div class="header-section">
|
||||
<div class="header-content">
|
||||
<div class="header-icon">
|
||||
<i class="pi pi-comments" style="font-size: 1.36rem"></i>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Chat with WizardAI</h1>
|
||||
<p class="page-subtitle">
|
||||
Contextualized on Project: <strong>{{ userPrefStore.user.selectedProject.fe_name }}</strong>
|
||||
<span v-if="userPrefStore.user.selectedApplication">
|
||||
• Application: <strong>{{ userPrefStore.user.selectedApplication.fe_name }}</strong>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</style>
|
||||
<!-- Chat Section -->
|
||||
<div class="chat-section">
|
||||
<div class="chat-card">
|
||||
<ChatClient />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-page-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced Header Section */
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeInDown 0.5s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
padding: 1.36rem;
|
||||
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Chat Section */
|
||||
.chat-section {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-page-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user