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:
@@ -10,8 +10,11 @@
|
||||
transition:
|
||||
transform var(--layout-section-transition-duration),
|
||||
left var(--layout-section-transition-duration);
|
||||
background-color: var(--surface-overlay);
|
||||
border-radius: var(--content-border-radius);
|
||||
background: linear-gradient(180deg, rgba(161, 0, 255, 0.05) 0%, rgba(123, 0, 204, 0.02) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(161, 0, 255, 0.1);
|
||||
box-shadow: 0 4px 20px rgba(161, 0, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
@@ -25,8 +28,10 @@
|
||||
font-size: 0.857rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
color: #A100FF;
|
||||
margin: 0.75rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid rgba(161, 0, 255, 0.2);
|
||||
}
|
||||
|
||||
> a {
|
||||
@@ -65,13 +70,14 @@
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--content-border-radius);
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
background-color var(--element-transition-duration),
|
||||
box-shadow var(--element-transition-duration);
|
||||
all 0.3s ease;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.layout-submenu-toggler {
|
||||
@@ -82,11 +88,25 @@
|
||||
|
||||
&.active-route {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
color: #A100FF;
|
||||
background: linear-gradient(90deg, rgba(161, 0, 255, 0.1) 0%, rgba(161, 0, 255, 0.05) 100%);
|
||||
border-left-color: #A100FF;
|
||||
box-shadow: 0 2px 8px rgba(161, 0, 255, 0.15);
|
||||
|
||||
.layout-menuitem-icon {
|
||||
color: #A100FF;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
background: linear-gradient(90deg, rgba(161, 0, 255, 0.08) 0%, rgba(161, 0, 255, 0.03) 100%);
|
||||
border-left-color: rgba(161, 0, 255, 0.5);
|
||||
transform: translateX(3px);
|
||||
|
||||
.layout-menuitem-icon {
|
||||
color: #A100FF;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 0 2rem;
|
||||
background-color: var(--surface-card);
|
||||
background: linear-gradient(135deg, rgba(161, 0, 255, 0.85) 0%, #A100FF 50%, #7B00CC 100%);
|
||||
box-shadow: 0 4px 20px rgba(161, 0, 255, 0.25);
|
||||
transition: left var(--layout-section-transition-duration);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -22,12 +23,19 @@
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
border-radius: var(--content-border-radius);
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
svg {
|
||||
width: 3rem;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
text-shadow: 0 2px 8px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -39,16 +47,17 @@
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--text-color);
|
||||
transition: background-color var(--element-transition-duration);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -65,8 +74,9 @@
|
||||
}
|
||||
|
||||
&.layout-topbar-action-highlight {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--primary-contrast-color);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +92,7 @@
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-topbar-menu-content {
|
||||
@@ -92,6 +103,13 @@
|
||||
.layout-config-menu {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-topbar-theme {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
/* Global button rounded corners */
|
||||
.p-button {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.p-inputtext,
|
||||
.p-dropdown,
|
||||
.p-calendar,
|
||||
.p-multiselect {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
// h2 {
|
||||
// font-size: 1em !important;
|
||||
// font-weight: bold !important;
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
<template>
|
||||
<div class="chat-wrapper p-p-3">
|
||||
<div class="p-d-flex p-flex-column" style="height: 100%">
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div v-for="(msg, index) in messages" :key="index" :class="['chat-message', msg.sender]">
|
||||
<div class="message-bubble">
|
||||
<div v-if="msg.sender === 'bot'">
|
||||
<MarkdownViewer class="editor" theme="light" previewTheme="github" v-model="msg.text" :key="index" />
|
||||
</div>
|
||||
<p v-else>{{ msg.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p-inputGroup class="p-mt-2" style="width: 100%">
|
||||
<p-inputText v-model="message" placeholder="Ask anything..." @keyup.enter="sendMessage" />
|
||||
|
||||
<p-button label="Ask" icon="pi pi-send" class="p-button-primary" @click="sendMessage" />
|
||||
<p-button label="Clear" icon="pi pi-trash" @click="clearHistory" class="p-button-warn" />
|
||||
<!-- <p-button icon="pi pi-cog" @click="showSettings = !showSettings" class="p-button-normal" /> -->
|
||||
</p-inputGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { marked } from 'marked';
|
||||
import Button from 'primevue/button';
|
||||
@@ -233,81 +209,356 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-wrapper">
|
||||
<div class="chat-container">
|
||||
<!-- Messages Area -->
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div v-if="messages.length === 0" class="empty-state">
|
||||
<i class="pi pi-comments empty-icon"></i>
|
||||
<p class="empty-text">No messages yet. Start a conversation!</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(msg, index) in messages" :key="index" :class="['chat-message', msg.sender]">
|
||||
<div class="message-avatar" v-if="msg.sender === 'bot'">
|
||||
<i class="pi pi-android"></i>
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
<div v-if="msg.sender === 'bot'" class="bot-message-content">
|
||||
<MarkdownViewer class="markdown-content" theme="light" previewTheme="github" v-model="msg.text" :key="index" />
|
||||
</div>
|
||||
<p v-else class="user-message-content">{{ msg.text }}</p>
|
||||
</div>
|
||||
<div class="message-avatar" v-if="msg.sender === 'user'">
|
||||
<i class="pi pi-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="chat-input-section">
|
||||
<p-inputGroup class="chat-input-group">
|
||||
<p-inputText v-model="message" placeholder="Ask anything about your scenario..." @keyup.enter="sendMessage" :disabled="waitingData" class="chat-input" />
|
||||
<p-button label="Send" icon="pi pi-send" @click="sendMessage" :disabled="waitingData || !message.trim()" severity="success" class="send-button" />
|
||||
<p-button icon="pi pi-trash" @click="clearHistory" :disabled="waitingData || messages.length === 0" severity="danger" v-tooltip.top="'Clear chat history'" class="clear-button" />
|
||||
</p-inputGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap');
|
||||
@import 'md-editor-v3/lib/style.css';
|
||||
|
||||
.md-editor {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.chat-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
height: 82vh;
|
||||
}
|
||||
|
||||
/* Card delle impostazioni */
|
||||
.chat-settings-card {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Area messaggi */
|
||||
.chat-messages {
|
||||
background: #f4f4f4;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
/* Singolo messaggio */
|
||||
.chat-message {
|
||||
margin-bottom: 1rem;
|
||||
.chat-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
/* Messaggi dell'utente a destra */
|
||||
/* Messages Area */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #A100FF #f0f0f0;
|
||||
}
|
||||
|
||||
/* Webkit scrollbar styling */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #A100FF;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #a0aec0;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Chat Message */
|
||||
.chat-message {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* User messages aligned right */
|
||||
.chat-message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Bolla del messaggio */
|
||||
/* Bot messages aligned left */
|
||||
.chat-message.bot {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Message Avatar */
|
||||
.message-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-message.bot .message-avatar {
|
||||
background: linear-gradient(135deg, #A100FF 0%, #7B00CC 100%);
|
||||
color: white;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.chat-message.user .message-avatar {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
/* Message Bubble */
|
||||
.message-bubble {
|
||||
max-width: 70%;
|
||||
padding: 0.75rem;
|
||||
border-radius: 15px;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 16px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Stile messaggi del bot */
|
||||
.chat-message.bot .message-bubble {
|
||||
background: #e1e1e1;
|
||||
color: #000;
|
||||
background: white;
|
||||
color: #2d3748;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-message.bot .message-bubble:hover {
|
||||
box-shadow: 0 4px 12px rgba(161, 0, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Stile messaggi dell'utente */
|
||||
.chat-message.user .message-bubble {
|
||||
background: #6f3ff5; /* Sostituisci con il colore desiderato */
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.3);
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Esempio di pulsanti "outlined" personalizzati */
|
||||
.p-button-outlined {
|
||||
border: 1px solid #6f3ff5; /* Adatta al tuo tema */
|
||||
color: #6f3ff5;
|
||||
/* Message Content */
|
||||
.bot-message-content {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
.p-button-outlined.p-button-danger {
|
||||
border: 1px solid #f44336; /* Rosso */
|
||||
color: #f44336;
|
||||
|
||||
.user-message-content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Markdown Viewer Styling */
|
||||
.markdown-content {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-content :deep(*) {
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre) {
|
||||
background: #f7fafc !important;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.markdown-content :deep(code) {
|
||||
background: #f7fafc !important;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
color: #A100FF !important;
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre code) {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
color: #2d3748 !important;
|
||||
}
|
||||
|
||||
/* Input Section */
|
||||
.chat-input-section {
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-input-group {
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input:deep(.p-inputtext) {
|
||||
padding: 0.875rem 1rem;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chat-input:deep(.p-inputtext:focus) {
|
||||
border-color: #A100FF;
|
||||
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
|
||||
}
|
||||
|
||||
.chat-input:deep(.p-inputtext:disabled) {
|
||||
background: #f7fafc;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.send-button,
|
||||
.clear-button {
|
||||
min-width: auto;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.send-button:deep(button) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.send-button:not(:disabled):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
|
||||
}
|
||||
|
||||
.clear-button:not(:disabled):hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.3);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
min-height: 500px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 85%;
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
|
||||
.chat-input-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.send-button :deep(.p-button-label) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.chat-input:disabled,
|
||||
.send-button:disabled,
|
||||
.clear-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
65
src/components/ExecutionChatSection.vue
Normal file
65
src/components/ExecutionChatSection.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import ChatClient from '@/components/ChatClient.vue';
|
||||
|
||||
const props = defineProps({
|
||||
executionId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
scenarioName: {
|
||||
type: String,
|
||||
default: 'Scenario'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="executionId" />
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
80
src/components/ExecutionInputSection.vue
Normal file
80
src/components/ExecutionInputSection.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import ExecutionInputTable from '@/components/ExecutionInputTable.vue';
|
||||
import { useScenarioRating } from '@/composables/useScenarioRating';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
executionId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
inputs: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
scenario: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
execScenario: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
showRating: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['download-file', 'rating-updated']);
|
||||
|
||||
// Rating composable
|
||||
const {
|
||||
rating: ratingValue,
|
||||
updateRating,
|
||||
canUpdate
|
||||
} = useScenarioRating(
|
||||
computed(() => props.executionId),
|
||||
computed(() => props.execScenario)
|
||||
);
|
||||
|
||||
// Initialize rating from props
|
||||
if (props.rating !== null) {
|
||||
ratingValue.value = props.rating;
|
||||
}
|
||||
|
||||
const handleRatingUpdate = async (newRating) => {
|
||||
const success = await updateRating(newRating);
|
||||
if (success) {
|
||||
emit('rating-updated', ratingValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadFile = (filePath) => {
|
||||
emit('download-file', filePath);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel class="mt-6">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">Execution Input for ID {{ executionId }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #icons>
|
||||
<div v-if="showRating" class="flex justify-end">
|
||||
<Rating :modelValue="ratingValue" :stars="5" :readonly="!canUpdate" @change="handleRatingUpdate($event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ExecutionInputTable :inputs="inputs" :scenario="scenario" @download-file="handleDownloadFile" />
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
263
src/components/ExecutionResponsePanel.vue
Normal file
263
src/components/ExecutionResponsePanel.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<script setup>
|
||||
import ChangeImpactOutputViewer from '@/components/ChangeImpactOutputViewer.vue';
|
||||
import MarkdownViewer from '@/components/MarkdownViewer.vue';
|
||||
import { useFileDownload } from '@/composables/useFileDownload';
|
||||
import { useFileProcessing } from '@/composables/useFileProcessing';
|
||||
import { useScenarioRating } from '@/composables/useScenarioRating';
|
||||
import { ScenarioExecutionStore } from '@/stores/ScenarioExecutionStore';
|
||||
import JsonEditorVue from 'json-editor-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Unified execution response panel for both history and live execution views
|
||||
* Consolidates OldExecutionResponsePanel and WorkflowResponsePanel
|
||||
*/
|
||||
const props = defineProps({
|
||||
scenario: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
execScenario: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
scenarioOutput: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
executionId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'history', // 'history' or 'live'
|
||||
validator: (value) => ['history', 'live'].includes(value)
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showRating: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
erroredExecution: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['download-file', 'rating-updated']);
|
||||
|
||||
// Composables
|
||||
const scenarioExecutionStore = ScenarioExecutionStore();
|
||||
const { downloadCodegenieFile } = useFileDownload();
|
||||
const { fileContent: processedFileContent, fileType: processedFileType, showFileContent } = useFileProcessing();
|
||||
const {
|
||||
rating,
|
||||
updateRating: updateRatingComposable,
|
||||
canUpdate
|
||||
} = useScenarioRating(
|
||||
computed(() => props.executionId),
|
||||
computed(() => props.execScenario)
|
||||
);
|
||||
|
||||
// Local state
|
||||
const debug_modal = ref(false);
|
||||
const localExecScenario = ref({});
|
||||
|
||||
// Computed
|
||||
const localScenarioOutput = computed(() => props.scenarioOutput);
|
||||
const hasError = computed(() => {
|
||||
if (props.erroredExecution) return true;
|
||||
if (props.mode === 'history') {
|
||||
return props.execScenario?.latestStepStatus === 'ERROR';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const errorText = computed(() => {
|
||||
if (props.errorMessage) return props.errorMessage;
|
||||
if (props.mode === 'history' && props.execScenario?.latestStepOutput) {
|
||||
return props.execScenario.latestStepOutput;
|
||||
}
|
||||
return 'Execution failed.';
|
||||
});
|
||||
|
||||
// Get file type from props or scenario
|
||||
const displayFileType = computed(() => {
|
||||
// For history mode, use parent's processed file type
|
||||
if (props.mode === 'history' && props.execScenario?.scenario?.steps?.[0]?.attributes?.['codegenie_output_type']) {
|
||||
return props.execScenario.scenario.steps[0].attributes['codegenie_output_type'];
|
||||
}
|
||||
return processedFileType.value || '';
|
||||
});
|
||||
|
||||
// Get file content for display
|
||||
const displayFileContent = computed(() => {
|
||||
return processedFileContent.value;
|
||||
});
|
||||
|
||||
// Check if execution is done (for FILE type)
|
||||
const isExecutionDone = computed(() => {
|
||||
if (props.mode === 'history') {
|
||||
return props.execScenario?.execSharedMap?.status === 'DONE';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const openDebug = async () => {
|
||||
try {
|
||||
if (props.mode === 'live') {
|
||||
const resp = await scenarioExecutionStore.getScenarioExecution(props.executionId);
|
||||
localExecScenario.value = resp;
|
||||
} else {
|
||||
localExecScenario.value = props.execScenario;
|
||||
}
|
||||
debug_modal.value = true;
|
||||
} catch (error) {
|
||||
console.error('Error opening debug:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (props.mode === 'history') {
|
||||
emit('download-file', props.scenarioOutput);
|
||||
} else {
|
||||
downloadCodegenieFile(props.scenarioOutput, props.executionId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRatingUpdate = async (newRating) => {
|
||||
const success = await updateRatingComposable(newRating);
|
||||
if (success) {
|
||||
emit('rating-updated', rating.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Expose for parent if needed (backward compatibility)
|
||||
defineExpose({
|
||||
showFileContent,
|
||||
fileType: processedFileType
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel class="mt-6">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">Workflow Response</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #icons>
|
||||
<div class="flex justify-end gap-2">
|
||||
<!-- Rating (live mode only) -->
|
||||
<div v-if="mode === 'live' && showRating" class="flex">
|
||||
<Rating :modelValue="rating" :stars="5" @change="handleRatingUpdate($event)" />
|
||||
</div>
|
||||
|
||||
<!-- Debug Button -->
|
||||
<div>
|
||||
<Button severity="secondary" rounded @click="openDebug" v-tooltip.left="'View execution info'">
|
||||
<i class="pi pi-code"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-if="hasError" class="card flex flex-col gap-4 w-full">
|
||||
<p class="text-red-500 font-bold">Error: {{ errorText }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else class="card flex flex-col gap-4 w-full">
|
||||
<!-- CIA Output -->
|
||||
<div v-if="scenario.outputType === 'ciaOutput'">
|
||||
<ChangeImpactOutputViewer :scenario_output="scenarioOutput" />
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-else-if="isLoading">
|
||||
<div class="flex justify-center mt-4">
|
||||
<jellyfish-loader :loading="isLoading" scale="1" color="#A100FF" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Output -->
|
||||
<div v-else>
|
||||
<!-- FILE Type: Download Button -->
|
||||
<div v-if="displayFileType === 'FILE' && isExecutionDone">
|
||||
<ul class="file-list">
|
||||
<li class="file-item">
|
||||
sf_document-{{ executionId }}
|
||||
<Button icon="pi pi-download" class="p-button-text p-button-sm" label="Download" @click="handleDownload" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- MARKDOWN Type: Rendered HTML -->
|
||||
<div v-else-if="displayFileType === 'MARKDOWN' && displayFileContent">
|
||||
<div v-html="displayFileContent" class="markdown-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- JSON Type: Formatted JSON -->
|
||||
<div v-else-if="displayFileType === 'JSON' && displayFileContent">
|
||||
<pre>{{ displayFileContent }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Legacy file output type -->
|
||||
<div v-else-if="scenario.outputType === 'file'">
|
||||
<Button icon="pi pi-download" label="Download File" class="p-button-primary" @click="handleDownload" />
|
||||
</div>
|
||||
|
||||
<!-- Default: Markdown Viewer -->
|
||||
<div v-else>
|
||||
<MarkdownViewer class="editor" :modelValue="localScenarioOutput" background-color="white" padding="20px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<!-- Debug Modal -->
|
||||
<Dialog v-model:visible="debug_modal" maximizable modal :header="scenario.name" :style="{ width: '75%' }" :breakpoints="{ '1199px': '75vw', '575px': '90vw' }">
|
||||
<div class="flex">
|
||||
<div class="card flex flex-col gap-4 w-full">
|
||||
<JsonEditorVue v-model="localExecScenario" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.editor ol {
|
||||
list-style-type: decimal !important;
|
||||
}
|
||||
|
||||
.editor ul {
|
||||
list-style-type: disc !important;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
padding: 1rem;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
61
src/components/ExecutionResponseSection.vue
Normal file
61
src/components/ExecutionResponseSection.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import ExecutionResponsePanel from '@/components/ExecutionResponsePanel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
scenario: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
execScenario: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
scenarioOutput: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
executionId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'history',
|
||||
validator: (value) => ['history', 'live'].includes(value)
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showRating: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['download-file', 'rating-updated']);
|
||||
|
||||
const handleDownload = (output) => {
|
||||
emit('download-file', output);
|
||||
};
|
||||
|
||||
const handleRatingUpdate = (newRating) => {
|
||||
emit('rating-updated', newRating);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExecutionResponsePanel
|
||||
:scenario="scenario"
|
||||
:exec-scenario="execScenario"
|
||||
:scenario-output="scenarioOutput"
|
||||
:execution-id="executionId"
|
||||
:mode="mode"
|
||||
:is-loading="isLoading"
|
||||
:show-rating="showRating"
|
||||
@download-file="handleDownload"
|
||||
@rating-updated="handleRatingUpdate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -64,13 +64,13 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div v-if="isLoading" class="flex flex-col items-center">
|
||||
<div class="flex justify-center mt-4">
|
||||
<jellyfish-loader :loading="isLoading" scale="1" color="#A100FF" />
|
||||
</div>
|
||||
<div v-if="message && message.includes('/')">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
<div v-else>Starting execution...</div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<jellyfish-loader :loading="isLoading" scale="1" color="#A100FF" />
|
||||
</div>
|
||||
<div class="flex justify-center" style="margin-bottom: 30px">
|
||||
<p>Time elapsed: </p>
|
||||
<div class="timer">{{ elapsedTime }}</div>
|
||||
|
||||
@@ -42,12 +42,80 @@ export default {
|
||||
const showMermaidModal = ref(false);
|
||||
const modalSvgContent = ref('');
|
||||
|
||||
// Sanitize mermaid syntax to prevent XSS and injection attacks
|
||||
const sanitizeMermaidCode = (code) => {
|
||||
if (!code) return '';
|
||||
|
||||
// Remove script tags and event handlers
|
||||
let sanitized = code
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '');
|
||||
|
||||
// Remove potentially dangerous mermaid directives
|
||||
sanitized = sanitized
|
||||
.replace(/%%\{init:.*?}%%/gs, '')
|
||||
.replace(/click\s+\w+\s+call\s+.*?$/gm, '')
|
||||
.replace(/click\s+\w+\s+href\s+["']javascript:.*?["']/gi, '');
|
||||
|
||||
// Allow only safe URLs in click handlers (http, https, mailto)
|
||||
sanitized = sanitized.replace(/click\s+(\w+)\s+href\s+["'](?!https?:\/\/|mailto:)([^"']*)["']/gi, 'click $1 href "#"');
|
||||
|
||||
// Remove HTML entities that could be used for obfuscation
|
||||
sanitized = sanitized.replace(/<script/gi, '').replace(/<iframe/gi, '');
|
||||
|
||||
// Fix mermaid syntax issues: Remove or replace problematic characters in node labels
|
||||
// Square brackets and parentheses inside node labels cause parse errors
|
||||
|
||||
// Replace content inside parentheses node labels - remove nested brackets and parens
|
||||
sanitized = sanitized.replace(/(\w+)\(([^)]+)\)/g, (match, nodeId, label) => {
|
||||
// Remove square brackets and extra parentheses from label
|
||||
const cleanLabel = label
|
||||
.replace(/[\[\]()]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return `${nodeId}(${cleanLabel})`;
|
||||
});
|
||||
|
||||
// Replace content inside square bracket node labels - remove nested brackets and parens
|
||||
sanitized = sanitized.replace(/(\w+)\[([^\]]+)\]/g, (match, nodeId, label) => {
|
||||
// Remove square brackets and parentheses from label
|
||||
const cleanLabel = label
|
||||
.replace(/[\[\]()]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return `${nodeId}[${cleanLabel}]`;
|
||||
});
|
||||
|
||||
// Replace content inside curly braces node labels - remove nested special chars
|
||||
sanitized = sanitized.replace(/(\w+)\{([^}]+)\}/g, (match, nodeId, label) => {
|
||||
const cleanLabel = label
|
||||
.replace(/[\[\]()]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return `${nodeId}{${cleanLabel}}`;
|
||||
});
|
||||
|
||||
// Limit maximum length to prevent DoS
|
||||
const maxLength = 50000;
|
||||
if (sanitized.length > maxLength) {
|
||||
sanitized = sanitized.substring(0, maxLength);
|
||||
}
|
||||
|
||||
return sanitized.trim();
|
||||
};
|
||||
|
||||
// Initialize markdown-it with plugins (without markdown-it-mermaid)
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
// Return empty string to let markdown-it-highlightjs handle it
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.use(markdownItHighlightjs, {
|
||||
auto: true,
|
||||
@@ -70,8 +138,9 @@ export default {
|
||||
const langName = info ? info.split(/\s+/g)[0] : '';
|
||||
|
||||
if (langName === 'mermaid') {
|
||||
// Wrap mermaid code in a div with a class
|
||||
return `<div class="mermaid">${md.utils.escapeHtml(token.content)}</div>`;
|
||||
// Sanitize and wrap mermaid code in a div with a class
|
||||
const sanitizedContent = sanitizeMermaidCode(token.content);
|
||||
return `<div class="mermaid">${md.utils.escapeHtml(sanitizedContent)}</div>`;
|
||||
}
|
||||
|
||||
// Let highlight.js handle all other code blocks through markdown-it-highlightjs
|
||||
@@ -118,7 +187,7 @@ export default {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: props.theme === 'dark' ? 'dark' : 'default',
|
||||
securityLevel: 'loose',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: 'inherit',
|
||||
logLevel: 'error',
|
||||
flowchart: {
|
||||
@@ -156,7 +225,8 @@ export default {
|
||||
if (element.hasAttribute('data-processed')) continue;
|
||||
|
||||
try {
|
||||
const code = element.textContent;
|
||||
const rawCode = element.textContent;
|
||||
const code = sanitizeMermaidCode(rawCode);
|
||||
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { svg } = await mermaid.render(id, code);
|
||||
|
||||
@@ -723,16 +793,20 @@ export default {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
}
|
||||
|
||||
/* Code blocks - styled by highlight.js with atom-one-dark theme */
|
||||
/* Code blocks - styled by highlight.js theme */
|
||||
.markdown-content :deep(pre) {
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
background: #282c34; /* Atom One Dark background */
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre code) {
|
||||
.markdown-content :deep(pre code),
|
||||
.markdown-content :deep(pre code.hljs) {
|
||||
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: #282c34; /* Atom One Dark background */
|
||||
color: #abb2bf; /* Atom One Dark text color */
|
||||
}
|
||||
|
||||
/* Copy button for code blocks */
|
||||
|
||||
57
src/composables/useBase64Decoder.js
Normal file
57
src/composables/useBase64Decoder.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Composable for base64 decoding operations
|
||||
* @returns {Object} Base64 utilities
|
||||
*/
|
||||
export function useBase64Decoder() {
|
||||
/**
|
||||
* Decode base64 string to Uint8Array
|
||||
*/
|
||||
const decodeToBytes = (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);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
} catch (error) {
|
||||
console.error('Error decoding base64 to bytes:', error);
|
||||
throw new Error('Failed to decode base64 string');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode base64 string to text
|
||||
*/
|
||||
const decodeToText = (base64String) => {
|
||||
try {
|
||||
const bytes = decodeToBytes(base64String);
|
||||
return new TextDecoder().decode(bytes);
|
||||
} catch (error) {
|
||||
console.error('Error decoding base64 to text:', error);
|
||||
throw new Error('Failed to decode base64 to text');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode base64 string to Blob
|
||||
*/
|
||||
const decodeToBlob = (base64String, mimeType = 'application/octet-stream') => {
|
||||
try {
|
||||
const bytes = decodeToBytes(base64String);
|
||||
return new Blob([bytes], { type: mimeType });
|
||||
} catch (error) {
|
||||
console.error('Error decoding base64 to blob:', error);
|
||||
throw new Error('Failed to decode base64 to blob');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
decodeToBytes,
|
||||
decodeToText,
|
||||
decodeToBlob
|
||||
};
|
||||
}
|
||||
37
src/composables/useChatToggle.js
Normal file
37
src/composables/useChatToggle.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for managing chat panel toggle state
|
||||
* @returns {Object} Chat toggle utilities and state
|
||||
*/
|
||||
export function useChatToggle() {
|
||||
const chatEnabled = ref(false);
|
||||
|
||||
/**
|
||||
* Enable chat panel
|
||||
*/
|
||||
const enableChat = () => {
|
||||
chatEnabled.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable chat panel
|
||||
*/
|
||||
const disableChat = () => {
|
||||
chatEnabled.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle chat panel
|
||||
*/
|
||||
const toggleChat = () => {
|
||||
chatEnabled.value = !chatEnabled.value;
|
||||
};
|
||||
|
||||
return {
|
||||
chatEnabled,
|
||||
enableChat,
|
||||
disableChat,
|
||||
toggleChat
|
||||
};
|
||||
}
|
||||
74
src/composables/useErrorHandler.js
Normal file
74
src/composables/useErrorHandler.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
/**
|
||||
* Composable for standardized error handling with toast notifications
|
||||
* @returns {Object} Error handling utilities
|
||||
*/
|
||||
export function useErrorHandler() {
|
||||
const toast = useToast();
|
||||
|
||||
/**
|
||||
* Show success toast
|
||||
*/
|
||||
const showSuccess = (summary, detail, life = 3000) => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary,
|
||||
detail,
|
||||
life
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show error toast
|
||||
*/
|
||||
const showError = (summary, detail, life = 3000) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary,
|
||||
detail,
|
||||
life
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show warning toast
|
||||
*/
|
||||
const showWarning = (summary, detail, life = 3000) => {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary,
|
||||
detail,
|
||||
life
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show info toast
|
||||
*/
|
||||
const showInfo = (summary, detail, life = 3000) => {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary,
|
||||
detail,
|
||||
life
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle error with logging and toast
|
||||
*/
|
||||
const handleError = (error, userMessage = 'An error occurred', consoleMessage = null) => {
|
||||
console.error(consoleMessage || userMessage, error);
|
||||
showError('Error', userMessage);
|
||||
};
|
||||
|
||||
return {
|
||||
toast,
|
||||
showSuccess,
|
||||
showError,
|
||||
showWarning,
|
||||
showInfo,
|
||||
handleError
|
||||
};
|
||||
}
|
||||
141
src/composables/useFileDownload.js
Normal file
141
src/composables/useFileDownload.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ScenarioExecutionStore } from '@/stores/ScenarioExecutionStore';
|
||||
import { ref } from 'vue';
|
||||
import { useBase64Decoder } from './useBase64Decoder';
|
||||
import { useErrorHandler } from './useErrorHandler';
|
||||
|
||||
/**
|
||||
* Composable for file download operations
|
||||
* @returns {Object} Download utilities and state
|
||||
*/
|
||||
export function useFileDownload() {
|
||||
const scenarioExecutionStore = ScenarioExecutionStore();
|
||||
const { decodeToBlob } = useBase64Decoder();
|
||||
const { showError } = useErrorHandler();
|
||||
|
||||
const isDownloading = ref(false);
|
||||
const baseUploadDir = '/mnt/hermione_storage/hermione/file_input_scenarios/';
|
||||
|
||||
/**
|
||||
* Download file from server (via store)
|
||||
*/
|
||||
const downloadFile = async (filePath, executionId) => {
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
|
||||
// Normalize path by removing base directory if present
|
||||
let relativePath = filePath;
|
||||
if (filePath.startsWith(baseUploadDir)) {
|
||||
relativePath = filePath.substring(baseUploadDir.length);
|
||||
}
|
||||
|
||||
await scenarioExecutionStore.downloadFile(relativePath, executionId);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
showError('Error', 'Error downloading file. Please try again.');
|
||||
throw error;
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download file from base64 string
|
||||
*/
|
||||
const downloadBase64File = (base64String, fileName, mimeType = 'application/octet-stream') => {
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
|
||||
const blob = decodeToBlob(base64String, mimeType);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading base64 file:', error);
|
||||
showError('Error', 'Error downloading file. Please try again.');
|
||||
throw error;
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download CodeGenie document (DOCX format)
|
||||
*/
|
||||
const downloadCodegenieFile = (base64String, executionId) => {
|
||||
try {
|
||||
const fileName = `sf_document-${executionId}.docx`;
|
||||
const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
downloadBase64File(base64String, fileName, mimeType);
|
||||
} catch (error) {
|
||||
console.error('Error downloading CodeGenie file:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download zip file from JSZip object
|
||||
*/
|
||||
const downloadZipFile = async (zipData, fileName) => {
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
|
||||
const blob = await zipData.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading zip file:', error);
|
||||
showError('Error', 'Error downloading zip file. Please try again.');
|
||||
throw error;
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Download CSV file
|
||||
*/
|
||||
const downloadCsvFile = (csvContent, fileName) => {
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading CSV file:', error);
|
||||
showError('Error', 'Error downloading CSV file. Please try again.');
|
||||
throw error;
|
||||
} finally {
|
||||
isDownloading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isDownloading,
|
||||
downloadFile,
|
||||
downloadBase64File,
|
||||
downloadCodegenieFile,
|
||||
downloadZipFile,
|
||||
downloadCsvFile
|
||||
};
|
||||
}
|
||||
135
src/composables/useFileProcessing.js
Normal file
135
src/composables/useFileProcessing.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import JSZip from 'jszip';
|
||||
import { marked } from 'marked';
|
||||
import { ref } from 'vue';
|
||||
import { useBase64Decoder } from './useBase64Decoder';
|
||||
|
||||
/**
|
||||
* Composable for file processing operations (zip extraction, base64 decoding, markdown/JSON parsing)
|
||||
* @returns {Object} File processing utilities and state
|
||||
*/
|
||||
export function useFileProcessing() {
|
||||
const { decodeToBytes, decodeToText } = useBase64Decoder();
|
||||
|
||||
const fileContent = ref('');
|
||||
const fileType = ref('');
|
||||
const fileNames = ref([]);
|
||||
const fileNamesOutput = ref([]);
|
||||
const zipInput = ref(null);
|
||||
const zipOutput = ref(null);
|
||||
|
||||
/**
|
||||
* Extract files from base64 encoded zip
|
||||
*/
|
||||
const extractFiles = async (base64String, type = 'input', zipRef = null) => {
|
||||
try {
|
||||
const bytes = decodeToBytes(base64String);
|
||||
const zipData = await JSZip.loadAsync(bytes);
|
||||
|
||||
// Store zip reference if provided
|
||||
if (zipRef) {
|
||||
if (type === 'input') {
|
||||
zipInput.value = zipData;
|
||||
} else {
|
||||
zipOutput.value = zipData;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract file names based on type
|
||||
if (type === 'input') {
|
||||
fileNames.value = getFileNamesInput(zipData);
|
||||
} else {
|
||||
fileNamesOutput.value = getFileNames(zipData);
|
||||
}
|
||||
|
||||
return zipData;
|
||||
} catch (error) {
|
||||
console.error('Error extracting zip:', error);
|
||||
if (type === 'input') {
|
||||
fileNames.value = [];
|
||||
} else {
|
||||
fileNamesOutput.value = [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show file content from base64 string (supports MARKDOWN, JSON)
|
||||
*/
|
||||
const showFileContent = (base64String, type) => {
|
||||
try {
|
||||
const textContent = decodeToText(base64String);
|
||||
|
||||
if (type === 'MARKDOWN') {
|
||||
fileContent.value = marked(textContent);
|
||||
fileType.value = 'MARKDOWN';
|
||||
} else if (type === 'JSON') {
|
||||
const jsonObject = JSON.parse(textContent);
|
||||
fileContent.value = JSON.stringify(jsonObject, null, 2);
|
||||
fileType.value = 'JSON';
|
||||
} else {
|
||||
fileContent.value = 'File type not supported.';
|
||||
fileType.value = type;
|
||||
}
|
||||
|
||||
return fileContent.value;
|
||||
} catch (error) {
|
||||
fileContent.value = 'Error while parsing the file.';
|
||||
console.error('Error showing file content:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get file names from zip (full paths)
|
||||
*/
|
||||
const getFileNamesInput = (zipData) => {
|
||||
const files = [];
|
||||
zipData.forEach((relativePath, file) => {
|
||||
if (!file.dir) {
|
||||
files.push(relativePath);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get file names from zip (file names only)
|
||||
*/
|
||||
const getFileNames = (zipData) => {
|
||||
const files = [];
|
||||
zipData.forEach((relativePath, file) => {
|
||||
if (!file.dir) {
|
||||
const fileName = relativePath.split('/').pop();
|
||||
files.push(fileName);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset all file processing state
|
||||
*/
|
||||
const reset = () => {
|
||||
fileContent.value = '';
|
||||
fileType.value = '';
|
||||
fileNames.value = [];
|
||||
fileNamesOutput.value = [];
|
||||
zipInput.value = null;
|
||||
zipOutput.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
fileType,
|
||||
fileNames,
|
||||
fileNamesOutput,
|
||||
zipInput,
|
||||
zipOutput,
|
||||
extractFiles,
|
||||
showFileContent,
|
||||
getFileNames,
|
||||
getFileNamesInput,
|
||||
reset
|
||||
};
|
||||
}
|
||||
87
src/composables/usePolling.js
Normal file
87
src/composables/usePolling.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import axios from 'axios';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for polling backend API for execution status
|
||||
* @returns {Object} Polling utilities and state
|
||||
*/
|
||||
export function usePolling() {
|
||||
const pollingInterval = ref(null);
|
||||
const isPolling = ref(false);
|
||||
const pollingData = ref(null);
|
||||
|
||||
/**
|
||||
* Start polling with custom callback
|
||||
*/
|
||||
const startPolling = (callback, interval = 2000) => {
|
||||
if (pollingInterval.value) {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
isPolling.value = true;
|
||||
pollingInterval.value = setInterval(callback, interval);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop polling
|
||||
*/
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
isPolling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Poll backend API for execution status
|
||||
*/
|
||||
const pollBackendAPI = async (executionId, onSuccess, onError, onComplete) => {
|
||||
try {
|
||||
const response = await axios.get('/execution?id=' + executionId);
|
||||
pollingData.value = response.data;
|
||||
|
||||
const status = response.data.latestStepStatus;
|
||||
|
||||
if (status === 'OK') {
|
||||
stopPolling();
|
||||
if (onSuccess) onSuccess(response.data);
|
||||
} else if (status === 'ERROR') {
|
||||
stopPolling();
|
||||
if (onError) onError(response.data);
|
||||
}
|
||||
|
||||
if (onComplete) onComplete(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error polling backend:', error);
|
||||
stopPolling();
|
||||
if (onError) onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start polling for execution status with callbacks
|
||||
*/
|
||||
const startExecutionPolling = (executionId, callbacks = {}, interval = 2000) => {
|
||||
const { onSuccess, onError, onComplete } = callbacks;
|
||||
|
||||
startPolling(() => {
|
||||
pollBackendAPI(executionId, onSuccess, onError, onComplete);
|
||||
}, interval);
|
||||
};
|
||||
|
||||
// Cleanup on component unmount
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
return {
|
||||
pollingInterval,
|
||||
isPolling,
|
||||
pollingData,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
pollBackendAPI,
|
||||
startExecutionPolling
|
||||
};
|
||||
}
|
||||
70
src/composables/useScenarioRating.js
Normal file
70
src/composables/useScenarioRating.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ScenarioService } from '@/service/ScenarioService';
|
||||
import { UserPrefStore } from '@/stores/UserPrefStore';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useErrorHandler } from './useErrorHandler';
|
||||
|
||||
/**
|
||||
* Composable for scenario execution rating functionality
|
||||
* @param {Ref|number} executionId - Execution ID (can be ref or number)
|
||||
* @param {Ref|Object} execution - Execution object (optional, for permission check)
|
||||
* @returns {Object} Rating utilities and state
|
||||
*/
|
||||
export function useScenarioRating(executionId, execution = null) {
|
||||
const userPrefStore = UserPrefStore();
|
||||
const { showSuccess, showError } = useErrorHandler();
|
||||
|
||||
const rating = ref(null);
|
||||
const isUpdating = ref(false);
|
||||
|
||||
/**
|
||||
* Check if current user can update rating
|
||||
*/
|
||||
const canUpdate = computed(() => {
|
||||
if (!execution || !execution.value) return false;
|
||||
return execution.value.executedByUsername === userPrefStore.getUser.username;
|
||||
});
|
||||
|
||||
/**
|
||||
* Update scenario execution rating
|
||||
*/
|
||||
const updateRating = async (newRating) => {
|
||||
isUpdating.value = true;
|
||||
|
||||
try {
|
||||
const execId = typeof executionId === 'object' ? executionId.value : executionId;
|
||||
const ratingValue = typeof newRating === 'object' ? newRating.value : newRating;
|
||||
|
||||
const response = await ScenarioService.updateScenarioExecRating(execId, ratingValue);
|
||||
|
||||
if (response.data === 'OK') {
|
||||
rating.value = ratingValue;
|
||||
showSuccess('Success', 'Rating updated with success.');
|
||||
return true;
|
||||
} else {
|
||||
showError('Error', 'Error updating rating. Try later.');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while updating rating:', error);
|
||||
showError('Error', 'Error updating rating.');
|
||||
return false;
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize rating from execution data
|
||||
*/
|
||||
const initializeRating = (initialRating) => {
|
||||
rating.value = initialRating;
|
||||
};
|
||||
|
||||
return {
|
||||
rating,
|
||||
isUpdating,
|
||||
canUpdate,
|
||||
updateRating,
|
||||
initializeRating
|
||||
};
|
||||
}
|
||||
@@ -9,18 +9,23 @@ const userPrefStore = UserPrefStore();
|
||||
const route = useRouter();
|
||||
|
||||
const model = ref([
|
||||
|
||||
{
|
||||
label: 'Scenarios',
|
||||
items: [
|
||||
{ label: 'Available Scenarios', icon: 'pi pi-fw pi-id-card', to: '/home' },
|
||||
{ label: 'Execution List', icon: 'pi pi-fw pi-list', command: () => {
|
||||
{
|
||||
label: 'Execution List',
|
||||
icon: 'pi pi-fw pi-list',
|
||||
command: () => {
|
||||
route.push({ path: '/executions/all' });
|
||||
} },
|
||||
] },
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
items: [] } ,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
label: 'Canvas',
|
||||
items: [{ label: 'New Canvas', icon: 'pi pi-fw pi-pencil', to: '/mdcanvas' }]
|
||||
@@ -29,7 +34,6 @@ const model = ref([
|
||||
label: 'Chat',
|
||||
items: [{ label: 'Chat', icon: 'pi pi-fw pi-comments', to: '/chat' }]
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
@@ -95,8 +99,6 @@ function updateApplicationsMenu() {
|
||||
model.value[1].items.push(createScenarioItem(scenarios[0]));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//Aggiungi "Rev Eng Code" alla fine della lista
|
||||
model.value[1].items.push({
|
||||
label: 'Application Code',
|
||||
@@ -148,13 +150,193 @@ watch(() => userPrefStore.getSelApp, updateApplicationsMenu, { immediate: true }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modern-menu-container">
|
||||
<ul class="layout-menu">
|
||||
<template v-for="(item, i) in model" :key="item">
|
||||
<app-menu-item v-if="!item.separator" :item="item" :index="i"></app-menu-item>
|
||||
<!--<li v-if="item.separator" class="menu-separator"></li>
|
||||
<hr v-if="i === 0" class="menu-separator"/>-->
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.modern-menu-container {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.layout-menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* Professional section headers */
|
||||
:deep(.layout-root-menuitem) {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
> .layout-menuitem-root-text {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin: 1.25rem 0 0.5rem 0;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Clean link styling */
|
||||
:deep(a) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0 none;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
margin: 0.125rem 0;
|
||||
font-weight: 500;
|
||||
font-size: 0.9375rem;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
transition: color 0.2s ease;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-menuitem-text {
|
||||
flex: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.layout-submenu-toggler {
|
||||
font-size: 0.75rem;
|
||||
margin-left: auto;
|
||||
transition: transform 0.2s ease;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f5f9;
|
||||
color: #334155;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
&.active-route {
|
||||
background-color: #eff6ff;
|
||||
color: #1e40af;
|
||||
font-weight: 600;
|
||||
border-left-color: #667eea;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.layout-submenu-toggler {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #e0e7ff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active menu item styling */
|
||||
:deep(.active-menuitem) {
|
||||
> a {
|
||||
.layout-submenu-toggler {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Submenu styling */
|
||||
:deep(.layout-submenu) {
|
||||
margin: 0.25rem 0 0.5rem 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
padding: 0.5rem 1rem 0.5rem 2.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nested submenu levels */
|
||||
.layout-submenu {
|
||||
a {
|
||||
padding-left: 3.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Separator styling */
|
||||
:deep(.menu-separator) {
|
||||
height: 1px;
|
||||
background-color: #e2e8f0;
|
||||
margin: 0.75rem 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Animation transitions */
|
||||
:deep(.layout-submenu-enter-from),
|
||||
:deep(.layout-submenu-leave-to) {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:deep(.layout-submenu-enter-to),
|
||||
:deep(.layout-submenu-leave-from) {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.layout-submenu-leave-active) {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height 0.3s cubic-bezier(0, 1, 0, 1),
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.layout-submenu-enter-active) {
|
||||
overflow: hidden;
|
||||
transition:
|
||||
max-height 0.4s ease-in-out,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
:deep(.layout-sidebar::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
:deep(.layout-sidebar::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.layout-sidebar::-webkit-scrollbar-thumb) {
|
||||
background: #cbd5e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(.layout-sidebar::-webkit-scrollbar-thumb:hover) {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useLayout } from '@/layout/composables/layout';
|
||||
import { useAuth } from '@websanova/vue-auth/src/v3.js';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
|
||||
import { LoadingStore } from '../stores/LoadingStore.js';
|
||||
import { ScenarioExecutionStore } from '../stores/ScenarioExecutionStore.js';
|
||||
import { ScenarioStore } from '../stores/ScenarioStore.js';
|
||||
@@ -11,19 +10,18 @@ import { UserPrefStore } from '../stores/UserPrefStore.js';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { JellyfishLoader, RiseLoader } from "vue3-spinner";
|
||||
import { JellyfishLoader, RiseLoader } from 'vue3-spinner';
|
||||
import AppProfileMenu from './AppProfileMenu.vue';
|
||||
|
||||
const auth = useAuth();
|
||||
const route = useRoute();
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps(['page']);
|
||||
const userPrefStore = UserPrefStore();
|
||||
const scenario_store = ScenarioStore();
|
||||
const scenario_execution_store = ScenarioExecutionStore();
|
||||
const loadingStore = LoadingStore()
|
||||
const loadingStore = LoadingStore();
|
||||
const selectedApp = ref(userPrefStore.getSelApp);
|
||||
|
||||
const { onMenuToggle, toggleDarkMode, isDarkTheme } = useLayout();
|
||||
@@ -37,7 +35,6 @@ async function updateApplication() {
|
||||
|
||||
scenario_store.fetchApplicationScenarios();
|
||||
scenario_execution_store.fetchScenariosExecution();
|
||||
|
||||
}
|
||||
|
||||
function redirectProject() {
|
||||
@@ -49,7 +46,6 @@ function appUpdated() {
|
||||
}
|
||||
|
||||
watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -61,14 +57,14 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M27.017 30.3135C27.0057 30.5602 27 30.8085 27 31.0581C27 39.9267 34.1894 47.1161 43.0581 47.1161C51.9267 47.1161 59.1161 39.9267 59.1161 31.0581C59.1161 30.8026 59.1102 30.5485 59.0984 30.2959C60.699 30.0511 62.2954 29.7696 63.8864 29.4515L64.0532 29.4181C64.0949 29.9593 64.1161 30.5062 64.1161 31.0581C64.1161 42.6881 54.6881 52.1161 43.0581 52.1161C31.428 52.1161 22 42.6881 22 31.0581C22 30.514 22.0206 29.9747 22.0612 29.441L22.1136 29.4515C23.7428 29.7773 25.3777 30.0646 27.017 30.3135ZM52.4613 18.0397C49.8183 16.1273 46.5698 15 43.0581 15C39.54 15 36.2862 16.1313 33.6406 18.05C31.4938 17.834 29.3526 17.5435 27.221 17.1786C31.0806 12.7781 36.7449 10 43.0581 10C49.3629 10 55.0207 12.7708 58.8799 17.1612C56.7487 17.5285 54.6078 17.8214 52.4613 18.0397ZM68.9854 28.4316C69.0719 29.2954 69.1161 30.1716 69.1161 31.0581C69.1161 45.4495 57.4495 57.1161 43.0581 57.1161C28.6666 57.1161 17 45.4495 17 31.0581C17 30.1793 17.0435 29.3108 17.1284 28.4544L12.2051 27.4697C12.0696 28.6471 12 29.8444 12 31.0581C12 48.211 25.9052 62.1161 43.0581 62.1161C60.211 62.1161 74.1161 48.211 74.1161 31.0581C74.1161 29.8366 74.0456 28.6317 73.9085 27.447L68.9854 28.4316ZM69.6705 15.0372L64.3929 16.0927C59.6785 9.38418 51.8803 5 43.0581 5C34.2269 5 26.4218 9.39306 21.7089 16.1131L16.4331 15.0579C21.867 6.03506 31.7578 0 43.0581 0C54.3497 0 64.234 6.02581 69.6705 15.0372Z"
|
||||
fill="var(--primary-color)"
|
||||
fill="white"
|
||||
/>
|
||||
<mask id="path-2-inside-1" fill="var(--primary-color)">
|
||||
<mask id="path-2-inside-1" fill="white">
|
||||
<path d="M42.5 28.9252C16.5458 30.2312 0 14 0 14C0 14 26 22.9738 42.5 22.9738C59 22.9738 85 14 85 14C85 14 68.4542 27.6193 42.5 28.9252Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M0 14L5.87269 -3.01504L-12.6052 26.8495L0 14ZM42.5 28.9252L41.5954 10.948L42.5 28.9252ZM85 14L96.4394 27.8975L79.1273 -3.01504L85 14ZM0 14C-12.6052 26.8495 -12.5999 26.8546 -12.5946 26.8598C-12.5928 26.8617 -12.5874 26.8669 -12.5837 26.8706C-12.5762 26.8779 -12.5685 26.8854 -12.5605 26.8932C-12.5445 26.9088 -12.5274 26.9254 -12.5092 26.943C-12.4729 26.9782 -12.4321 27.0174 -12.387 27.0605C-12.2969 27.1467 -12.1892 27.2484 -12.0642 27.3646C-11.8144 27.5968 -11.4949 27.8874 -11.1073 28.2273C-10.3332 28.9063 -9.28165 29.7873 -7.96614 30.7967C-5.34553 32.8073 -1.61454 35.3754 3.11693 37.872C12.5592 42.8544 26.4009 47.7581 43.4046 46.9025L41.5954 10.948C32.6449 11.3983 25.2366 8.83942 19.9174 6.03267C17.2682 4.63475 15.2406 3.22667 13.9478 2.23478C13.3066 1.74283 12.8627 1.366 12.6306 1.16243C12.5151 1.06107 12.4538 1.00422 12.4485 0.999363C12.446 0.996981 12.4576 1.00773 12.4836 1.03256C12.4966 1.04498 12.5132 1.06094 12.5334 1.08055C12.5436 1.09035 12.5546 1.10108 12.5665 1.11273C12.5725 1.11855 12.5787 1.12461 12.5852 1.13091C12.5884 1.13405 12.5934 1.13895 12.595 1.14052C12.6 1.14548 12.6052 1.15049 0 14ZM43.4046 46.9025C59.3275 46.1013 72.3155 41.5302 81.3171 37.1785C85.8337 34.9951 89.4176 32.8333 91.9552 31.151C93.2269 30.3079 94.2446 29.5794 94.9945 29.0205C95.3698 28.7409 95.6788 28.503 95.92 28.3138C96.0406 28.2192 96.1443 28.1366 96.2309 28.067C96.2742 28.0321 96.3133 28.0005 96.348 27.9723C96.3654 27.9581 96.3817 27.9448 96.3969 27.9323C96.4045 27.9261 96.4119 27.9201 96.419 27.9143C96.4225 27.9114 96.4276 27.9072 96.4294 27.9057C96.4344 27.9016 96.4394 27.8975 85 14C73.5606 0.102497 73.5655 0.0985097 73.5703 0.0945756C73.5718 0.0933319 73.5765 0.0894438 73.5795 0.0869551C73.5856 0.0819751 73.5914 0.077195 73.597 0.0726136C73.6082 0.0634509 73.6185 0.055082 73.6278 0.0474955C73.6465 0.0323231 73.6614 0.0202757 73.6726 0.0112606C73.695 -0.00676378 73.7026 -0.0126931 73.6957 -0.00726687C73.6818 0.00363418 73.6101 0.0596753 73.4822 0.154983C73.2258 0.346025 72.7482 0.691717 72.0631 1.14588C70.6873 2.05798 68.5127 3.38259 65.6485 4.7672C59.8887 7.55166 51.6267 10.4432 41.5954 10.948L43.4046 46.9025ZM85 14C79.1273 -3.01504 79.1288 -3.01557 79.1303 -3.01606C79.1306 -3.01618 79.1319 -3.01664 79.1326 -3.01688C79.134 -3.01736 79.135 -3.0177 79.1356 -3.01791C79.1369 -3.01834 79.1366 -3.01823 79.1347 -3.01759C79.131 -3.01633 79.1212 -3.01297 79.1055 -3.00758C79.0739 -2.99681 79.0185 -2.97794 78.9404 -2.95151C78.7839 -2.89864 78.5366 -2.81564 78.207 -2.7068C77.5472 -2.48895 76.561 -2.16874 75.3165 -1.78027C72.8181 -1.00046 69.3266 0.039393 65.3753 1.07466C57.0052 3.26771 48.2826 4.97383 42.5 4.97383V40.9738C53.2174 40.9738 65.7448 38.193 74.4997 35.8992C79.1109 34.691 83.1506 33.4874 86.0429 32.5846C87.4937 32.1318 88.6676 31.7509 89.4942 31.478C89.9077 31.3414 90.2351 31.2317 90.4676 31.1531C90.5839 31.1138 90.6765 31.0823 90.7443 31.0591C90.7783 31.0475 90.806 31.038 90.8275 31.0306C90.8382 31.0269 90.8473 31.0238 90.8549 31.0212C90.8586 31.0199 90.862 31.0187 90.865 31.0177C90.8665 31.0172 90.8684 31.0165 90.8691 31.0163C90.871 31.0156 90.8727 31.015 85 14ZM42.5 4.97383C36.7174 4.97383 27.9948 3.26771 19.6247 1.07466C15.6734 0.039393 12.1819 -1.00046 9.68352 -1.78027C8.43897 -2.16874 7.4528 -2.48895 6.79299 -2.7068C6.46337 -2.81564 6.21607 -2.89864 6.05965 -2.95151C5.98146 -2.97794 5.92606 -2.99681 5.89453 -3.00758C5.87876 -3.01297 5.86897 -3.01633 5.86528 -3.01759C5.86344 -3.01823 5.86312 -3.01834 5.86435 -3.01791C5.86497 -3.0177 5.86597 -3.01736 5.86736 -3.01688C5.86805 -3.01664 5.86939 -3.01618 5.86973 -3.01606C5.87116 -3.01557 5.87269 -3.01504 0 14C-5.87269 31.015 -5.87096 31.0156 -5.86914 31.0163C-5.8684 31.0165 -5.86647 31.0172 -5.86498 31.0177C-5.86201 31.0187 -5.85864 31.0199 -5.85486 31.0212C-5.84732 31.0238 -5.83818 31.0269 -5.82747 31.0306C-5.80603 31.038 -5.77828 31.0475 -5.74435 31.0591C-5.67649 31.0823 -5.58388 31.1138 -5.46761 31.1531C-5.23512 31.2317 -4.9077 31.3414 -4.49416 31.478C-3.66764 31.7509 -2.49366 32.1318 -1.04289 32.5846C1.84938 33.4874 5.88908 34.691 10.5003 35.8992C19.2552 38.193 31.7826 40.9738 42.5 40.9738V4.97383Z"
|
||||
fill="var(--primary-color)"
|
||||
fill="white"
|
||||
mask="url(#path-2-inside-1)"
|
||||
/>
|
||||
</svg>
|
||||
@@ -87,13 +83,11 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
<!--<ProgressSpinner v-if="loadingStore.loadingType=='data'" style="width: 25px; height: 25px; margin-top: 6px" strokeWidth="2" fill="transparent"/>-->
|
||||
<JellyfishLoader v-if="loadingStore.loadingType == 'ai'" scale="0.5" color="#a100ff" style="width: 25px; height: 10px; margin-right: 25px; margin-top: -5px" />
|
||||
<RiseLoader v-if="loadingStore.loadingType == 'data'" color="#a100ff" style="scale: 0.5; height: 10px; margin-top: 5px" />
|
||||
|
||||
</div>
|
||||
<!-- <button @click="router.push('/mdcanvas')" class="layout-topbar-action" >
|
||||
<i class="pi pi-pencil"></i>
|
||||
</button > -->
|
||||
|
||||
|
||||
<!-- <div class="relative">
|
||||
<button
|
||||
v-styleclass="{ selector: '@next', enterFromClass: 'hidden', enterActiveClass: 'animate-scalein', leaveToClass: 'hidden', leaveActiveClass: 'animate-fadeout', hideOnOutsideClick: true }"
|
||||
@@ -104,9 +98,6 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
</button>
|
||||
<AppConfigurator />
|
||||
</div> -->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="layout-topbar-theme">
|
||||
<span class="flex items-center mt-2">Project:</span>
|
||||
@@ -124,15 +115,7 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
<div class="layout-topbar-theme">
|
||||
<span class="flex items-center mt-2">Application:</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-model="selectedApp"
|
||||
:options="userPrefStore.availableApp"
|
||||
optionLabel="fe_name"
|
||||
placeholder="Select an Application"
|
||||
class="dropdown-list menu-list"
|
||||
@change="updateApplication()"
|
||||
:disabled="isDropdownDisabled"
|
||||
/>
|
||||
<Dropdown v-model="selectedApp" :options="userPrefStore.availableApp" optionLabel="fe_name" placeholder="Select an Application" class="dropdown-list menu-list" @change="updateApplication()" :disabled="isDropdownDisabled" />
|
||||
|
||||
<button
|
||||
class="layout-topbar-menu-button layout-topbar-action"
|
||||
@@ -151,7 +134,6 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
<i class="pi pi-user"></i>
|
||||
</button>
|
||||
<AppProfileMenu />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,12 +146,55 @@ watch(() => userPrefStore.getSelApp, appUpdated, { immediate: true });
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1rem;
|
||||
min-width: 100px;
|
||||
|
||||
}
|
||||
|
||||
.topbar-project :deep(.p-button) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.topbar-project :deep(.p-button:hover) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
width: 200px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.menu-list :deep(.p-dropdown) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.menu-list :deep(.p-dropdown:hover) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.menu-list :deep(.p-dropdown-label) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-list :deep(.p-dropdown-trigger) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-list :deep(.p-dropdown:not(.p-disabled).p-focus) {
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
</style>
|
||||
30
src/main.js
30
src/main.js
@@ -63,17 +63,25 @@ const pinia = createPinia();
|
||||
const preset = definePreset(Nora, {
|
||||
semantic: {
|
||||
primary: {
|
||||
50: '{violet.50}',
|
||||
100: '{violet.100}',
|
||||
200: '{violet.200}',
|
||||
300: '{violet.300}',
|
||||
400: '{violet.400}',
|
||||
500: '{violet.500}',
|
||||
600: '{violet.600}',
|
||||
700: '{violet.700}',
|
||||
800: '{violet.800}',
|
||||
900: '{violet.900}',
|
||||
950: '{violet.950}'
|
||||
50: '#F5E6FF',
|
||||
100: '#E6CCFF',
|
||||
200: '#CC99FF',
|
||||
300: '#B366FF',
|
||||
400: '#9933FF',
|
||||
500: '#A100FF',
|
||||
600: '#8800D9',
|
||||
700: '#7B00CC',
|
||||
800: '#6600A6',
|
||||
900: '#4D007F',
|
||||
950: '#330052'
|
||||
},
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
lg: '12px',
|
||||
xl: '16px'
|
||||
},
|
||||
colorScheme: {
|
||||
light: {
|
||||
|
||||
@@ -38,7 +38,7 @@ const router = createRouter({
|
||||
{
|
||||
path: 'exec-history',
|
||||
name: 'scenario-exec-history',
|
||||
component: () => import('@/views/pages/OldScenarioExec.vue')
|
||||
component: () => import('@/views/pages/ScenarioExecHistory.vue')
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
|
||||
@@ -35,6 +35,11 @@ export const ScenarioService = {
|
||||
return axios.get(`/scenarios/${id}`);
|
||||
},
|
||||
|
||||
// Get full scenario execution details by ID
|
||||
getScenarioExecutionById(id) {
|
||||
return axios.get('/execution?id=' + id);
|
||||
},
|
||||
|
||||
// Nuovo metodo per eseguire uno scenario in modo asincrono
|
||||
executeScenarioAsync(data) {
|
||||
return axios.post('/scenarios/execute-async', data);
|
||||
|
||||
127
src/utils/csvExport.js
Normal file
127
src/utils/csvExport.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* CSV export utilities
|
||||
* Consolidates CSV generation and download logic
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert array of objects to CSV string
|
||||
* @param {Array} data - Array of objects to convert
|
||||
* @param {Array} columns - Column definitions with field and header
|
||||
* @returns {string} CSV string
|
||||
*/
|
||||
export function convertToCSV(data, columns) {
|
||||
if (!data || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create header row
|
||||
const header = columns.map((col) => col.header || col.field).join(',');
|
||||
|
||||
// Create data rows
|
||||
const rows = data.map((item) => {
|
||||
return columns
|
||||
.map((col) => {
|
||||
let value = item[col.field];
|
||||
|
||||
// Handle null/undefined
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
// Handle objects and arrays
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
// Escape quotes and wrap in quotes if contains comma or quotes
|
||||
value = String(value).replace(/"/g, '""');
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export data to CSV file
|
||||
* @param {Array} data - Data to export
|
||||
* @param {Array} columns - Column definitions
|
||||
* @param {string} fileName - Output file name
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
export function exportToCSV(data, columns, fileName = 'export.csv') {
|
||||
try {
|
||||
const csv = convertToCSV(data, columns);
|
||||
|
||||
if (!csv) {
|
||||
console.warn('No data to export');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error exporting CSV:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for CSV export
|
||||
* @param {Date|string} date - Date to format
|
||||
* @param {boolean} includeTime - Include time in output
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function formatDateForCSV(date, includeTime = true) {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
|
||||
if (!includeTime) {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize data for CSV export
|
||||
* @param {*} value - Value to sanitize
|
||||
* @returns {string} Sanitized value
|
||||
*/
|
||||
export function sanitizeForCSV(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
71
src/utils/formDataProcessor.js
Normal file
71
src/utils/formDataProcessor.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Process form data for multiselect inputs
|
||||
* Converts multiselect arrays into _id and _name JSON strings
|
||||
* Consolidates processFormData logic from multiple files
|
||||
* @param {Object} formData - Form data object
|
||||
* @param {Array} multiselectFields - Array of multiselect field names
|
||||
* @returns {Object} Processed form data
|
||||
*/
|
||||
export function processFormData(formData, multiselectFields = []) {
|
||||
const processed = { ...formData };
|
||||
|
||||
multiselectFields.forEach((field) => {
|
||||
if (Array.isArray(processed[field]) && processed[field].length > 0) {
|
||||
// Convert array to _id and _name format
|
||||
const ids = processed[field].map((item) => (typeof item === 'object' ? item.id : item));
|
||||
const names = processed[field].map((item) => (typeof item === 'object' ? item.name : item));
|
||||
|
||||
processed[`${field}_id`] = JSON.stringify(ids);
|
||||
processed[`${field}_name`] = JSON.stringify(names);
|
||||
|
||||
// Remove original array field
|
||||
delete processed[field];
|
||||
}
|
||||
});
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multiselect data from _id and _name format
|
||||
* Reverse operation of processFormData
|
||||
* @param {Object} data - Data object with _id and _name fields
|
||||
* @param {Array} multiselectFields - Array of multiselect field names
|
||||
* @returns {Object} Parsed data with arrays
|
||||
*/
|
||||
export function parseMultiselectData(data, multiselectFields = []) {
|
||||
const parsed = { ...data };
|
||||
|
||||
multiselectFields.forEach((field) => {
|
||||
const idField = `${field}_id`;
|
||||
const nameField = `${field}_name`;
|
||||
|
||||
if (parsed[idField] && parsed[nameField]) {
|
||||
try {
|
||||
const ids = JSON.parse(parsed[idField]);
|
||||
const names = JSON.parse(parsed[nameField]);
|
||||
|
||||
parsed[field] = ids.map((id, index) => ({
|
||||
id,
|
||||
name: names[index]
|
||||
}));
|
||||
|
||||
delete parsed[idField];
|
||||
delete parsed[nameField];
|
||||
} catch (error) {
|
||||
console.error(`Error parsing multiselect field ${field}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate multiselect field value
|
||||
* @param {*} value - Value to validate
|
||||
* @returns {boolean} True if valid multiselect value
|
||||
*/
|
||||
export function isValidMultiselectValue(value) {
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
}
|
||||
38
src/utils/inputComponents.js
Normal file
38
src/utils/inputComponents.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import Select from 'primevue/select';
|
||||
import Textarea from 'primevue/textarea';
|
||||
|
||||
/**
|
||||
* Map input type to PrimeVue component
|
||||
* Consolidates getInputComponent logic from multiple files
|
||||
* @param {string} type - Input type (text, textarea, select, multiselect)
|
||||
* @returns {Component} PrimeVue component
|
||||
*/
|
||||
export function getInputComponent(type) {
|
||||
const components = {
|
||||
text: InputText,
|
||||
textarea: Textarea,
|
||||
select: Select,
|
||||
multiselect: MultiSelect
|
||||
};
|
||||
|
||||
return components[type] || InputText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available input types
|
||||
* @returns {Array} Array of input type names
|
||||
*/
|
||||
export function getInputTypes() {
|
||||
return ['text', 'textarea', 'select', 'multiselect'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input type is valid
|
||||
* @param {string} type - Input type to check
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
export function isValidInputType(type) {
|
||||
return getInputTypes().includes(type);
|
||||
}
|
||||
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,31 +335,46 @@ const chatDisabled = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Header Section -->
|
||||
<div class="flex items-center justify-between p-1">
|
||||
<h1>{{ scenario.name }}</h1>
|
||||
<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 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 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>
|
||||
|
||||
<!-- 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" />
|
||||
<!-- 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="grid grid-cols-2 md:grid-cols-1">
|
||||
<div v-for="input in scenario.inputs" :key="input.name">
|
||||
<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'">
|
||||
<div v-if="input.type === 'singlefile' || input.type === 'singlefile_acceptall'" class="input-group">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
@@ -375,7 +390,7 @@ const chatDisabled = () => {
|
||||
</div>
|
||||
|
||||
<!-- Multi File Upload -->
|
||||
<div v-else-if="input.type === 'multifile'">
|
||||
<div v-else-if="input.type === 'multifile'" class="input-group">
|
||||
<ScenarioFileUpload
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
@@ -391,7 +406,7 @@ const chatDisabled = () => {
|
||||
</div>
|
||||
|
||||
<!-- Multi Select with Dynamic Picker -->
|
||||
<div v-else-if="input.type === 'multiselect'" class="mt-4">
|
||||
<div v-else-if="input.type === 'multiselect'" class="input-group">
|
||||
<DynamicPicker
|
||||
v-model="formData[input.name]"
|
||||
:input-name="input.name"
|
||||
@@ -407,10 +422,10 @@ const chatDisabled = () => {
|
||||
</div>
|
||||
|
||||
<!-- Other Input Types -->
|
||||
<div v-else>
|
||||
<label :for="input.name"
|
||||
><b>{{ input.label }}</b></label
|
||||
>
|
||||
<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>
|
||||
@@ -418,60 +433,514 @@ const chatDisabled = () => {
|
||||
</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>
|
||||
<!-- 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 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 -->
|
||||
<div class="timer-section">
|
||||
<ExecutionTimer :is-loading="loading_data" :message="scenario_response_message" />
|
||||
</div>
|
||||
|
||||
<!-- Workflow Response Panel -->
|
||||
<div v-if="data_loaded && !chat_enabled">
|
||||
<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>
|
||||
|
||||
<!-- Chat Panel -->
|
||||
<div v-if="data_loaded && chat_enabled" class="mt-4">
|
||||
<Panel class="mt-6">
|
||||
<!-- Enhanced Chat Panel -->
|
||||
<div v-if="data_loaded && chat_enabled" class="chat-section">
|
||||
<Panel class="chat-panel">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="font-bold">Chat with WizardAI</span>
|
||||
<div class="chat-header">
|
||||
<i class="pi pi-comments chat-icon"></i>
|
||||
<span class="chat-title">Chat with WizardAI</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card flex flex-col gap-4 w-full">
|
||||
<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
|
||||
}]
|
||||
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,15 +81,12 @@ 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') {
|
||||
console.log('selectedScenario: im in');
|
||||
filters.value['scenario.name'].constraints[0].value = selectedScenario;
|
||||
|
||||
} else {
|
||||
filters.value['scenario.name'].constraints[0].value = null;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -329,10 +154,519 @@ const goToScenarioExec = (execScenarioItem) => {
|
||||
// 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;
|
||||
}
|
||||
@@ -348,7 +682,6 @@ const goToScenarioExec = (execScenarioItem) => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.editor ol {
|
||||
list-style-type: decimal !important;
|
||||
}
|
||||
@@ -356,6 +689,4 @@ list-style-type: decimal !important;
|
||||
.editor ul {
|
||||
list-style-type: disc !important;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,89 +1,11 @@
|
||||
<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 router = useRouter();
|
||||
const layout = ref('grid');
|
||||
const options = ref(['list', 'grid']);
|
||||
|
||||
@@ -92,48 +14,213 @@ import { UserPrefStore } from '../../stores/UserPrefStore.js';
|
||||
|
||||
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.fetchScenariosCross();
|
||||
scenario_store.fetchScenarios();
|
||||
if (userPrefStore.getSelApp != null) {
|
||||
scenario_store.fetchApplicationScenarios();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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>
|
||||
|
||||
<!-- 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