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:
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user