Merge branch 'master' into develop

This commit is contained in:
2026-01-19 17:43:48 +01:00
50 changed files with 13230 additions and 3560 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
<script>
import MarkdownViewer from '@/components/MarkdownViewer.vue';
import { ref } from 'vue';
export default {
name: 'MarkdownDemo',
components: {
MarkdownViewer
},
setup() {
const sampleMarkdown = ref(`# MarkdownViewer Component Demo
## Features Demonstration
### 1. Text Formatting
This component supports **bold**, *italic*, ~~strikethrough~~, and \`inline code\`.
> This is a blockquote. You can use it for important notes or citations.
### 2. Lists
#### Unordered List
- Feature 1: Mermaid diagram support
- Feature 2: Table copy/export
- Feature 3: Syntax highlighting
- Nested item 1
- Nested item 2
#### Ordered List
1. First step
2. Second step
3. Third step
### 3. Code Blocks with Syntax Highlighting
#### JavaScript Example
\`\`\`javascript
function calculateTotal(items) {
return items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
}
const cart = [
{ name: 'Apple', price: 1.5, quantity: 3 },
{ name: 'Orange', price: 2.0, quantity: 2 }
];
console.log('Total:', calculateTotal(cart));
\`\`\`
#### Python Example
\`\`\`python
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Generate first 10 Fibonacci numbers
fib_sequence = [fibonacci(i) for i in range(10)]
print(f"Fibonacci sequence: {fib_sequence}")
\`\`\`
#### JSON Example
\`\`\`json
{
"name": "MarkdownViewer",
"version": "1.0.0",
"features": [
"mermaid",
"tables",
"syntax-highlighting"
],
"config": {
"theme": "github",
"enableCopy": true
}
}
\`\`\`
### 4. Tables with Copy & Export Features
Try the **Copy Table** and **Export CSV** buttons above each table!
#### Feature Comparison
| Feature | MdPreview | MarkdownViewer | Notes |
|---------|-----------|----------------|-------|
| Basic Markdown | ✅ | ✅ | Full support |
| Mermaid Diagrams | ❌ | ✅ | Flow, sequence, class diagrams |
| Table Copy | ❌ | ✅ | One-click copy to clipboard |
| CSV Export | ❌ | ✅ | Download as CSV file |
| Syntax Highlighting | ✅ | ✅ | 100+ languages |
| Extensibility | Limited | ✅ | Plugin-based architecture |
| Performance | Good | Excellent | Optimized rendering |
#### Project Status
| Task | Status | Progress | Due Date |
|------|--------|----------|----------|
| Component Development | Complete | 100% | 2024-01-15 |
| Documentation | Complete | 100% | 2024-01-16 |
| Testing | In Progress | 75% | 2024-01-20 |
| Deployment | Pending | 0% | 2024-01-25 |
### 5. Mermaid Diagrams
#### Flowchart
\`\`\`mermaid
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> E[Fix Issue]
E --> B
C --> F[Deploy]
F --> G[End]
\`\`\`
#### Sequence Diagram
\`\`\`mermaid
sequenceDiagram
participant User
participant Frontend
participant Backend
participant Database
User->>Frontend: Click Submit
Frontend->>Backend: POST /api/data
Backend->>Database: INSERT query
Database-->>Backend: Success
Backend-->>Frontend: 200 OK
Frontend-->>User: Show success message
\`\`\`
#### Class Diagram
\`\`\`mermaid
classDiagram
class MarkdownViewer {
+String modelValue
+String theme
+String previewTheme
+render()
+renderMermaid()
+addTableFunctionality()
}
class MarkdownIt {
+parse()
+render()
}
class Mermaid {
+initialize()
+render()
}
MarkdownViewer --> MarkdownIt
MarkdownViewer --> Mermaid
\`\`\`
#### Gantt Chart
\`\`\`mermaid
gantt
title Project Timeline
dateFormat YYYY-MM-DD
section Planning
Requirements :done, 2024-01-01, 5d
Design :done, 2024-01-06, 7d
section Development
Component :done, 2024-01-13, 10d
Testing :active, 2024-01-23, 7d
section Deployment
Staging :2024-01-30, 3d
Production :2024-02-02, 2d
\`\`\`
### 6. Links and Images
Check out the [markdown-it documentation](https://github.com/markdown-it/markdown-it) for more information.
Visit [Mermaid's official site](https://mermaid-js.github.io/) for diagram syntax.
### 7. Horizontal Rule
---
### 8. Task Lists
- [x] Create MarkdownViewer component
- [x] Add Mermaid support
- [x] Implement table copy/export
- [x] Add syntax highlighting
- [ ] Write comprehensive tests
- [ ] Create user documentation
### 9. Mathematical Expressions (if needed)
Inline math: The quadratic formula is $x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}$
---
## Try It Yourself!
You can modify the markdown content in the code to see how different elements are rendered. The component supports:
1. **All standard Markdown syntax**
2. **GitHub Flavored Markdown (GFM)**
3. **Mermaid diagrams** - flowcharts, sequence diagrams, class diagrams, Gantt charts, and more
4. **Interactive tables** - with copy and CSV export functionality
5. **Syntax highlighting** - for 100+ programming languages
### Performance Notes
- Mermaid diagrams are rendered asynchronously
- Tables are enhanced with interactive features
- Syntax highlighting is applied automatically
- The component is optimized for large documents
---
**Enjoy the power of modern Markdown rendering! 🚀**
`);
return {
sampleMarkdown
};
}
};
</script>
<template>
<div class="markdown-demo-page">
<div class="demo-header">
<h1>MarkdownViewer Component Demo</h1>
<p class="subtitle">A powerful Markdown viewer with Mermaid diagrams, interactive tables, and syntax highlighting</p>
</div>
<div class="demo-content">
<MarkdownViewer v-model="sampleMarkdown" theme="light" previewTheme="github" />
</div>
</div>
</template>
<style scoped>
.markdown-demo-page {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.demo-header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e0e0e0;
}
.demo-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.1rem;
color: #666;
margin: 0;
}
.demo-content {
background: #fff;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.markdown-demo-page {
padding: 1rem;
}
.demo-header h1 {
font-size: 2rem;
}
.demo-content {
padding: 1rem;
}
}
</style>

View File

@@ -78,9 +78,9 @@ import ProgressSpinner from 'primevue/progressspinner';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { LoadingStore } from '../../stores/LoadingStore.js';
import { UserPrefStore } from '../../stores/UserPrefStore.js';
import { ScenarioStore } from '../../stores/ScenarioStore.js';
import { ScenarioExecutionStore } from '../../stores/ScenarioExecutionStore.js';
import { ScenarioStore } from '../../stores/ScenarioStore.js';
import { UserPrefStore } from '../../stores/UserPrefStore.js';
const loadingStore = LoadingStore()
@@ -116,7 +116,7 @@ const scenario_execution_store = ScenarioExecutionStore();
return data.search
.toLowerCase()
.split(" ")
.every((v) => item.name.toLowerCase().includes(v));
.every((v) => item.fe_name.toLowerCase().includes(v));
});
} else {
return data.projects;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,997 @@
<template>
<div class="flex items-center justify-between p-1">
<h1>
{{ scenario.name }}
</h1>
</div>
<div class="flex items-center justify-between p-1">
<h2>
{{ scenario.description }}
</h2>
</div>
<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>
</div>
<div v-else class="flex mt-2">
<div class="card flex flex-col w-full">
<MdPreview :class="['markdown-content', 'ml-[-20px]']" v-model="scenario.hint" language="en-US" />
<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 v-if="input.type === 'singlefile' || input.type === 'singlefile_acceptall'">
<label :for="input.name">
<b>{{ input.label }}</b>
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="'Upload one document from the suggested types. Mandatory if you want to execute scenario.'"></i>
</label>
<div>
<FileUpload
:name="'MultiFileUpload'"
:customUpload="false"
:url="uploadUrlPR"
@upload="(event) => onUpload(event, 'SingleFileUpload')"
:multiple="false"
:accept="acceptedFormats"
auto
:showUploadButton="false"
:showCancelButton="false"
:maxFileSize="52428800"
:invalidFileSizeMessage="'Invalid file size, file size should be smaller than 20 MB'"
v-model:files="uploadedFiles"
@before-send="onBeforeSend"
>
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="pt-4">
<!-- Tabella per file in caricamento -->
<div v-if="uploadedFiles.length > 0">
<table class="table-auto w-full border-collapse border border-gray-200">
<thead>
<tr>
<th class="border border-gray-300 p-2">Name</th>
<th class="border border-gray-300 p-2">Dimension</th>
<th class="border border-gray-300 p-2">Status</th>
<th class="border border-gray-300 p-2">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(file, index) in uploadedFiles" :key="file.name + file.size" class="hover:bg-gray-50">
<td class="border border-gray-300 p-2">{{ file.name }}</td>
<td class="border border-gray-300 p-2">{{ formatSize(file.size) }}</td>
<td class="border border-gray-300 p-2">
<Badge value="UPLOADED" severity="success" />
</td>
<td class="border border-gray-300 p-2">
<Button label="Remove" @click="onRemove({ file, index }, removeUploadedFileCallback, 'SingleFileUpload')" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template #empty>
<div class="flex items-center justify-center flex-col">
<!-- <i class="pi pi-cloud-upload !border border-black !rounded-full !w-21 !h-21 !p-6 !text-4xl !text-muted-color" /> -->
<div class="!border !border-violet-600 !rounded-full !w-24 !h-24 flex items-center justify-center">
<i class="pi pi-cloud-upload !text-4xl !-violet-600"></i>
</div>
<p class="mt-2 mb-2 text-m">Drag and drop files here to upload.</p>
</div>
</template>
</FileUpload>
</div>
</div>
<div v-else-if="input.type === 'multifile'">
<label :for="input.name">
<b>{{ input.label }} </b>
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="'Upload others documents of .docx, .msg, .text type. Optional.'"></i>
</label>
<div>
<FileUpload
:name="'MultiFileUpload'"
:customUpload="false"
:url="uploadUrlOther"
@upload="(event) => onUpload(event, 'MultiFileUpload')"
:multiple="true"
accept=".msg,.txt,.docx"
auto
:showUploadButton="false"
:showCancelButton="false"
:maxFileSize="52428800"
v-model:files="uploadedFiles"
@before-send="onBeforeSend"
>
>
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
<div class="pt-4">
<!-- Tabella per file in caricamento -->
<div v-if="uploadedFiles.length > 0">
<table class="table-auto w-full border-collapse border border-gray-200">
<thead>
<tr>
<th class="border border-gray-300 p-2">Name</th>
<th class="border border-gray-300 p-2">Dimension</th>
<th class="border border-gray-300 p-2">Status</th>
<th class="border border-gray-300 p-2">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(file, index) in uploadedFiles" :key="file.name + file.size" class="hover:bg-gray-50">
<td class="border border-gray-300 p-2">{{ file.name }}</td>
<td class="border border-gray-300 p-2">{{ formatSize(file.size) }}</td>
<td class="border border-gray-300 p-2">
<Badge value="UPLOADED" severity="success" />
</td>
<td class="border border-gray-300 p-2">
<Button label="Remove" @click="onRemove({ file, index }, removeUploadedFileCallback, 'MultiFileUpload')" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<template #empty>
<div class="flex items-center justify-center flex-col">
<!-- <i class="pi pi-cloud-upload !border border-black !rounded-full !w-21 !h-21 !p-6 !text-4xl !text-muted-color" /> -->
<div class="!border !border-violet-600 !rounded-full !w-24 !h-24 flex items-center justify-center">
<i class="pi pi-cloud-upload !text-4xl !-violet-600"></i>
</div>
<p class="mt-2 mb-0 text-m">Drag and drop files here to upload.</p>
</div>
</template>
</FileUpload>
</div>
</div>
<div v-else-if="input.type === 'multiselect'" class="mt-4">
<DynamicPicker
v-model="formData[input.name]"
:input-name="input.name"
:label="input.label"
:data-source="input.dataSource || 'videoGroups'"
:options="getOptionsForInput(input)"
:disabled="loadingStore.exectuion_loading"
:loading="loadingOptionsFor[input.dataSource] || false"
:show-status="input.dataSource === 'ksDocuments'"
no-margin
@change="onDynamicPickerChange(input.name, $event)"
/>
</div>
<div v-else>
<label :for="input.name"
><b>{{ input.label }}</b></label
>
<div class="input-wrapper">
<component :is="getInputComponent(input.type)" :id="input.name" v-model="formData[input.name]" :options="input.options" class="full-width-input" :disabled="loadingStore.exectuion_loading" />
</div>
</div>
</div>
</div>
<div v-if="data_loaded && scenario.chatEnabled" class="flex justify-center">
<div v-if="!chat_enabled" class="flex gap-4 mt-6">
<Button :disabled="loadingStore.exectuion_loading || !isInputFilled" label="Execute" @click="execScenario" size="large" iconPos="right" icon="pi pi-cog"></Button>
<Button label="Open Chat" @click="chatEnabled" size="large" iconPos="right" icon="pi pi-comments"></Button>
</div>
<!-- <div v-else>
<Button label="Return to scenario" @click="chatDisabled" size="large" iconPos="right" icon="pi pi-backward"></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>
<div v-if="loading_data" class="flex flex-col items-center">
<div class="flex justify-center mt-4">
<jellyfish-loader :loading="loadingStore.exectuion_loading" scale="1" color="#A100FF" />
</div>
<div v-if="scenario_response_message && scenario_response_message.includes('/')">
<span>{{ scenario_response_message }}</span>
</div>
<div v-else>Starting execution...</div>
<div class="flex justify-center" style="margin-bottom: 30px">
<p>Time elapsed:&nbsp;</p>
<div id="timer" class="timer">00:00</div>
</div>
</div>
<div v-if="data_loaded && !chat_enabled">
<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">
<div class="flex">
<Rating :modelValue="rating" :stars="5" @change="updateRating($event)" />
</div>
<div>
<Button severity="secondary" rounded @click="openDebug" v-tooltip.left="'View execution info'">
<i class="pi pi-code"></i>
</Button>
</div>
</div>
</template>
<div v-if="errored_execution" class="card flex flex-col gap-4 w-full">
<div v-if="error_message">
<p class="text-red-500 font-bold">Error: {{ error_message }}</p>
</div>
<div v-else>
<p class="text-red-500 font-bold">Error: Execution failed.</p>
</div>
</div>
<div v-else class="card flex flex-col gap-4 w-full">
<div v-if="scenario.outputType == 'ciaOutput'">
<ChangeImpactOutputViewer :scenario_output="scenario_output" />
</div>
<div v-if="scenario.outputType == 'file'">
<Button icon="pi pi-download" label="Download File" class="p-button-primary" @click="downloadFile" />
</div>
<div v-else>
<div v-if="fileType == 'FILE'">
<ul>
<li class="file-item">
sf_document-{{ exec_id }}
<Button icon="pi pi-download" class="p-button-text p-button-sm" label="Download" @click="downloadFile()" />
</li>
</ul>
</div>
<div v-else-if="fileType == 'MARKDOWN'">
<div v-html="fileContent" class="markdown-content"></div>
</div>
<div v-else-if="fileType == 'JSON'">
<pre>{{ fileContent }}</pre>
</div>
<div v-else>
<MdPreview class="editor" v-model="scenario_output" language="en-US" />
</div>
</div>
</div>
</Panel>
<Dialog v-model:visible="debug_modal" maximizable modal :header="scenario.name" :style="{ width: '75%' }" :breakpoints="{ '1199px': '75vw', '575px': '90vw' }">
<div class="flex">
<div class="card flex flex-col gap-4 w-full">
<JsonEditorVue v-model="exec_scenario" />
</div>
</div>
</Dialog>
</div>
<div v-if="data_loaded && chat_enabled" class="mt-4">
<Panel class="mt-6">
<template #header>
<div class="flex items-center gap-2 mt-2">
<span class="font-bold">Chat with WizardAI</span>
</div>
</template>
<div class="card flex flex-col gap-4 w-full">
<ChatClient :scenarioExecutionId="exec_id" />
</div>
</Panel>
</div>
</template>
<script setup>
import ChangeImpactOutputViewer from '@/components/ChangeImpactOutputViewer.vue';
import ChatClient from '@/components/ChatClient.vue';
import DynamicPicker from '@/components/DynamicPicker.vue';
import { KSDocumentService } from '@/service/KSDocumentService';
import { FileUploadStore } from '@/stores/FileUploadStore';
import { KsVideoGroupStore } from '@/stores/KsVideoGroupStore';
import { LoadingStore } from '@/stores/LoadingStore';
import { ScenarioExecutionStore } from '@/stores/ScenarioExecutionStore';
import { UserPrefStore } from '@/stores/UserPrefStore';
import { useAuth } from '@websanova/vue-auth/src/v3.js';
import JsonEditorVue from 'json-editor-vue';
import JSZip from 'jszip';
import { marked } from 'marked';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import moment from 'moment';
import { usePrimeVue } from 'primevue/config';
import InputText from 'primevue/inputtext';
import MultiSelect from 'primevue/multiselect';
import Select from 'primevue/select';
import Textarea from 'primevue/textarea';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { JellyfishLoader } from 'vue3-spinner';
import { ScenarioService } from '../../service/ScenarioService';
const loadingStore = LoadingStore();
const scenarioExecutionStore = ScenarioExecutionStore();
const fileUploadStore = FileUploadStore();
const toast = useToast();
const zip = ref(null);
const route = useRoute();
const rating = ref(0);
const scenario = ref({});
const scenario_response = ref(null);
const scenario_output = ref(null);
const scenario_response_message = ref(null);
const error_message = ref(null);
const loading = ref(false);
const data_loaded = ref(false);
const loading_data = ref(false);
const formData = ref({});
const exec_id = ref(null);
const exec_scenario = ref({});
const debug_modal = ref(false);
const loadingOptionsFor = reactive({});
const ksDocuments = ref([]);
let pollingInterval = null;
const folderName = ref('');
const fileNamesOutput = ref([]);
const ksVideoGroupStore = KsVideoGroupStore();
const userPrefStore = UserPrefStore();
const videoGroups = ref([]);
// URL di upload
const uploadUrlBase = import.meta.env.VITE_BACKEND_URL;
const uploadUrl = ref('');
const uploadUrlPR = ref('');
const uploadUrlOther = ref('');
// File che l'utente ha selezionato
const uploadedFiles = ref([]);
const numberPrFiles = ref(0);
const acceptedFormats = ref('.docx');
// :url="`http://localhost:8081/uploadListFiles/${folderName}`"
const errored_execution = ref(false);
// Stato per l'ID univoco della cartella
const uniqueFolderId = ref(generateUniqueId());
const confirm = useConfirm();
const $primevue = usePrimeVue();
const files = ref([]);
const fileContent = ref('');
const fileType = ref('');
const reqMultiFile = ref(false);
const chat_enabled = ref(false);
const auth = useAuth();
let startTime = ref(null);
let timerInterval = ref(null);
function startTimer() {
startTime = Date.now();
timerInterval = setInterval(() => {
const elapsedTime = moment.duration(Date.now() - startTime);
document.getElementById('timer').textContent = moment.utc(elapsedTime.asMilliseconds()).format('mm:ss');
}, 1000);
}
function stopTimer() {
clearInterval(timerInterval);
}
const isInputFilled = computed(() => {
var isFilled = true;
if (scenario.value.inputs === undefined) {
console.log('No inputs found');
return false;
}
scenario.value.inputs.forEach((input) => {
const inputValue = formData.value[input.name];
// Controllo per input multiselect
if (input.type === 'multiselect') {
if (!inputValue || !Array.isArray(inputValue) || inputValue.length === 0) {
console.log('Multiselect input not filled: ', input.name);
isFilled = false;
}
}
// Controllo per altri tipi di input
else {
if (inputValue === undefined || inputValue === '') {
console.log('Input not filled: ', input.name);
isFilled = false;
}
}
});
return isFilled;
});
//When the component is dismissed stop the polling
onBeforeUnmount(() => {
stopPolling();
stopTimer();
});
onMounted(() => {
fetchScenario(route.params.id);
const newFolderName = fileUploadStore.generateUniqueFolderId();
folderName.value = newFolderName;
uploadUrl.value = uploadUrlBase + '/uploadListFiles/' + folderName.value;
uploadUrlPR.value = uploadUrl.value + '/PR';
uploadUrlOther.value = uploadUrl.value + '/OTHER';
console.log('Upload URL:', uploadUrl);
});
const loadVideoGroups = async () => {
await ksVideoGroupStore.fetchKsVideoGroup(userPrefStore.selectedProject.id).then(async () => {
videoGroups.value = [...(ksVideoGroupStore.ksVideoGroup || [])];
//Wait for all video counts to be fetched
videoGroups.value = await Promise.all(videoGroups.value);
});
};
// Ricarica i dati quando cambia il parametro `id`
watch(() => route.params.id, fetchScenario);
//Function to fetch scenarios
async function fetchScenario(id) {
chatDisabled();
scenario.value.inputs = null;
data_loaded.value = false;
formData.value = {};
loading.value = true;
try {
const response = await scenarioExecutionStore.fetchScenario(id);
scenario.value = response;
console.log('Scenario fetched:', scenario.value);
// Carica le opzioni necessarie basate sui dataSource presenti negli inputs
await loadOptionsForScenario();
if (scenario.value.inputs.some((input) => input.name === 'MultiFileUpload' || input.name === 'SingleFileUpload')) {
reqMultiFile.value = true;
}
if (scenario.value.inputs.some((input) => input.type === 'singlefile_acceptall')) {
reqMultiFile.value = false;
acceptedFormats.value = '';
//acceptedFormats.value = '.doc,.docx,.pdf,.msg,.txt,.xlx,.xlxs,.logs,.pptx,.json,.odt,.rtf,.xml,.html';
}
if (scenario.value.inputs.some((input) => input.type === 'singlefile')) {
reqMultiFile.value = false;
acceptedFormats.value = '.docx';
}
} catch (error) {
console.error('Error fetching scenario:', error);
} finally {
loading.value = false;
}
}
const onBeforeSend = (event) => {
const { xhr } = event; // Estraggo l'oggetto XMLHttpRequest
console.log('xhr', xhr);
var token = auth.token();
xhr.setRequestHeader('Authorization', 'Bearer ' + token); // Imposta il tipo di contenuto
};
const getInputComponent = (type) => {
switch (type) {
case 'text':
return InputText;
case 'textarea':
return Textarea;
case 'select':
return Select;
case 'multiselect':
return MultiSelect;
default:
return InputText;
}
};
const chatEnabled = () => {
chat_enabled.value = true;
};
const chatDisabled = () => {
chat_enabled.value = false;
};
const execScenario = async () => {
if (numberPrFiles.value !== 1 && reqMultiFile.value) {
toast.add({
severity: 'warn', // Tipo di notifica (errore)
summary: 'Attention', // Titolo della notifica
detail: 'You can upload only 1 PR file. Please remove others.' // Messaggio dettagliato
});
} else {
loading_data.value = true;
data_loaded.value = false;
rating.value = 0;
startTimer();
loadingStore.exectuion_loading = true;
// Crea una copia dei dati del form
const processedData = { ...formData.value };
// Elabora tutti i multiselect dinamici
if (scenario.value.inputs) {
scenario.value.inputs.forEach((input) => {
if (input.type === 'multiselect' && processedData[input.name]) {
const selectedItems = processedData[input.name];
if (Array.isArray(selectedItems) && selectedItems.length > 0) {
// Elaborazione per VideoGroups (backward compatibility)
processedData[`${input.name}_id`] = JSON.stringify(selectedItems.map((item) => item.id || item));
processedData[`${input.name}_name`] = JSON.stringify(selectedItems.map((item) => item.name || item.fileName || item));
// Rimuovi l'array originale
delete processedData[input.name];
}
}
});
}
const data = {
scenario_id: scenario.value.id,
inputs: processedData
};
try {
const response = await scenarioExecutionStore.executeScenario(data);
console.log('Response data exec 1:', response);
scenario_response.value = response;
scenario_response_message.value = response.message;
scenario_output.value = response.stringOutput;
exec_id.value = response.scenarioExecution_id;
loadingStore.setIdExecLoading(exec_id.value);
startPolling();
} catch (error) {
console.error('Error executing scenario:', error);
loadingStore.exectuion_loading = false;
}
}
};
const openDebug = async () => {
try {
const resp = await scenarioExecutionStore.getScenarioExecution(exec_id.value);
exec_scenario.value = resp;
debug_modal.value = true;
} catch (error) {
console.error('Error opening debug:', error);
}
};
const pollBackendAPI = async () => {
errored_execution.value = false;
try {
const response = await scenarioExecutionStore.getExecutionProgress(exec_id.value);
if (response.status == 'OK' || response.status == 'ERROR') {
console.log('Condition met, stopping polling.');
stopPolling();
stopTimer();
if (response.status == 'ERROR') {
errored_execution.value = true;
error_message.value = response.message;
}
loading_data.value = false;
data_loaded.value = true;
scenario_output.value = response.stringOutput;
console.log('Response data exec 2:', response);
exec_id.value = response.scenarioExecution_id;
scenario_response_message.value = null; //if != null, next scenario starts with old message
console.log('Scenario 3:', scenario.value);
// Controlla se l'array `inputs` contiene un elemento con `name = 'MultiFileUpload'`
if (scenario.value.inputs.some((input) => input.name === 'MultiFileUpload')) {
if (response.status == 'OK') {
// Accedi al primo step e controlla se esiste l'attributo `codegenie_output_type`
const firstStep = scenario.value.steps[0];
if (firstStep?.attributes?.['codegenie_output_type']) {
if (firstStep.attributes['codegenie_output_type'] == 'FILE') {
fileType.value = 'FILE';
} else if (firstStep.attributes['codegenie_output_type'] == 'MARKDOWN') {
fileType.value = 'MARKDOWN';
showFileContent(scenario_output.value, 'MARKDOWN');
} else if (firstStep.attributes['codegenie_output_type'] == 'JSON') {
fileType.value = 'JSON';
showFileContent(scenario_output.value, 'JSON');
}
}
} else {
console.log('Error in execution');
}
}
} else {
console.log('Condition not met, polling continues.');
scenario_response.value = response;
scenario_response_message.value = response.message;
}
} catch (error) {
console.error('Error polling backend API:', error);
}
};
const showFileContent = (base64String, type) => {
try {
// Decodifica la stringa Base64
const binaryString = atob(base64String);
const binaryLength = binaryString.length;
const bytes = new Uint8Array(binaryLength);
for (let i = 0; i < binaryLength; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Converti i byte in una stringa leggibile
const textContent = new TextDecoder().decode(bytes);
// Gestione del tipo di file
if (type === 'MARKDOWN') {
//fileType.value = 'markdown';
fileContent.value = marked(textContent); // Converte Markdown in HTML
} else if (type === 'JSON') {
//fileType.value = 'json';
const jsonObject = JSON.parse(textContent); // Parse JSON
fileContent.value = JSON.stringify(jsonObject, null, 2); // Formatta JSON
} else {
fileContent.value = 'Unsupported file type.';
}
} catch (error) {
fileContent.value = 'Errore while decoding or parsing file.';
console.error(error);
}
};
// Function to start polling
function startPolling() {
// Set polling interval (every 2.5 seconds in this case)
pollingInterval = setInterval(pollBackendAPI, 2500);
console.log('Polling started.');
}
// Function to stop polling
function stopPolling() {
clearInterval(pollingInterval);
loadingStore.exectuion_loading = false;
loadingStore.setIdExecLoading('');
console.log('Polling stopped.');
}
const extractFiles = async (base64String) => {
try {
// Decodifica la base64 in un array di byte
const byteCharacters = atob(base64String);
const byteNumbers = Array.from(byteCharacters, (char) => char.charCodeAt(0));
const byteArray = new Uint8Array(byteNumbers);
// Carica il file zip con JSZip
const zipData = await JSZip.loadAsync(byteArray);
zip.value = zipData;
// Ottieni tutti i file (compresi quelli nelle sottocartelle)
fileNamesOutput.value = getFileNames(zipData);
} catch (error) {
console.error('Error extracting zip:', error);
fileNamesOutput.value = [];
}
};
// Funzione ricorsiva per ottenere tutti i file (anche quelli dentro le cartelle)
const getFileNames = (zipData) => {
const files = [];
// Esplora tutti i file nel file zip, considerando anche le sottocartelle
zipData.forEach((relativePath, file) => {
if (!file.dir) {
// Escludiamo le cartelle
files.push(relativePath); // Aggiungiamo il percorso relativo del file
}
});
return files;
};
async function updateRating(newRating) {
ScenarioService.updateScenarioExecRating(exec_id.value, newRating.value)
.then((response) => {
console.log('response:', response);
if (response.data === 'OK') {
rating.value = newRating.value;
console.log('Rating successfully updated:', response.data);
toast.add({
severity: 'success', // Tipo di notifica (successo)
summary: 'Success', // Titolo della notifica
detail: 'Rating updated with success.', // Messaggio dettagliato
life: 3000 // Durata della notifica in millisecondi
});
} else {
console.error('Errore during rating update', response.data);
toast.add({
severity: 'error', // Tipo di notifica (errore)
summary: 'Error', // Titolo della notifica
detail: 'Error updating rating. Try later.', // Messaggio dettagliato
life: 3000 // Durata della notifica in millisecondi
});
}
})
.catch((error) => {
console.error('Error during backend call:', error);
});
}
// Funzione per generare un ID univoco
function generateUniqueId() {
return Date.now(); // Puoi usare anche UUID.randomUUID() o una libreria simile
}
const onRemove = async (event, removeUploadedFileCallback, type) => {
const { file, index } = event;
console.log('Removing file:', folderName.value);
try {
const response = await fileUploadStore.deleteFile(file.name, folderName.value);
if (response.status === 200) {
console.log('File removed successfully:', response.data);
// Mostra notifica di successo
toast.add({
severity: 'success',
summary: 'Success',
detail: 'File removed successfully!',
life: 3000
});
if (type === 'SingleFileUpload') {
numberPrFiles.value -= 1;
console.log('Number of PR files: ', numberPrFiles.value);
formData.value['SingleFileUpload'] = '';
}
// Aggiorna lista dei file caricati
removeUploadedFileCallback(index);
} else {
console.error('Failed to remove file:', response.statusText);
// Mostra notifica di errore
toast.add({
severity: 'error',
summary: 'Error',
detail: `Failed to remove file. Status: ${response.statusText}`,
life: 3000
});
}
} catch (error) {
console.error('Error while removing file:', error);
// Mostra notifica di errore
toast.add({
severity: 'error',
summary: 'Error',
detail: `Error while removing file: ${error.message}`,
life: 3000
});
}
};
const onUpload = (event, uploadType) => {
console.log('response upload ', event.xhr.response);
const { xhr } = event; // Estraggo l'oggetto XMLHttpRequest
if (xhr.status === 200) {
if (uploadType === 'SingleFileUpload') {
//formData.value['SingleFileUpload'] = "OK";
if (event.files && event.files.length > 0) {
console.log('File uploaded:', event.files);
formData.value['SingleFileUpload'] = event.files[0].name; // Nome del primo file
} else {
formData.value['SingleFileUpload'] = 'UnknownFile';
}
console.log('Length of uploaded files', event.files.length);
numberPrFiles.value += 1;
console.log('Number of PR files: ', numberPrFiles.value);
}
formData.value['MultiFileUpload'] = xhr.response;
console.log('Form value upload ', formData.value['MultiFileUpload']);
console.log('Upload successfully completed. Response:', xhr.response);
toast.add({
severity: 'success',
summary: 'Success',
detail: 'File uploaded successfully!',
life: 3000
});
console.log('Length of uploaded files', uploadedFiles.value.length);
} else {
// Errore durante l'upload
console.error('Error during upload. Status:', xhr.status, 'Response:', xhr.response);
toast.add({
severity: 'error',
summary: 'Error',
detail: `Failed to upload file. Status: ${xhr.status}`,
life: 3000
});
}
};
// Funzione per scaricare il file
const downloadZipFile = async (fileName) => {
if (!zip.value) return;
try {
// Estrai il file dallo zip
const fileContent = await zip.value.file(fileName).async('blob');
const url = URL.createObjectURL(fileContent);
// Crea un link per scaricare il file
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error(`Error downloading file "${fileName}":`, error);
}
};
function downloadFile() {
try {
// Converti la stringa base64 in un blob
const base64String = scenario_output.value;
const byteCharacters = atob(base64String);
const byteNumbers = Array.from(byteCharacters, (char) => char.charCodeAt(0));
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray]);
// Crea un link temporaneo per il download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sf_document-' + exec_id.value + '.docx'; // Specifica il nome del file
document.body.appendChild(a);
a.click();
// Rimuovi il link temporaneo
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error during file download:', error);
}
}
const formatSize = (bytes) => {
const k = 1024;
const sizes = $primevue.config.locale.fileSizeTypes;
if (bytes === 0) {
return `0 ${sizes[0]}`;
}
const i = Math.floor(Math.log(bytes) / Math.log(k));
const truncatedSize = Math.trunc(bytes / Math.pow(k, i)); // Troncamento del valore
return `${truncatedSize} ${sizes[i]}`;
};
// Metodi per la gestione delle picklist dinamiche
const getOptionsForInput = (input) => {
// Basato sul dataSource, restituisce le opzioni appropriate
switch (input.dataSource) {
case 'videoGroups':
return videoGroups.value;
case 'ksDocuments':
return ksDocuments.value;
default:
return [];
}
};
const onDynamicPickerChange = (inputName, value) => {
console.log(`Dynamic picker changed for ${inputName}:`, value);
formData.value[inputName] = value;
};
// Carica le opzioni necessarie basate sui dataSource presenti negli inputs dello scenario
const loadOptionsForScenario = async () => {
if (!scenario.value.inputs) return;
console.log('Loading options for scenario inputs...');
// Trova tutti i dataSource unici negli input multiselect
const dataSources = new Set();
scenario.value.inputs.forEach((input) => {
if (input.type === 'multiselect' && input.dataSource) {
dataSources.add(input.dataSource);
}
});
// Crea le funzioni di caricamento per ogni dataSource
const loadingPromises = Array.from(dataSources).map(async (dataSource) => {
try {
// Imposta lo stato di loading per questo dataSource
loadingOptionsFor[dataSource] = true;
console.log(`Loading options for dataSource: ${dataSource}`);
switch (dataSource) {
case 'videoGroups':
await loadVideoGroups();
break;
case 'ksDocuments':
const docsResponse = await KSDocumentService.getKSDocuments();
ksDocuments.value = docsResponse.data;
console.log(`Loaded ${ksDocuments.value.length} KS documents`);
break;
default:
console.warn(`Unknown dataSource: ${dataSource}`);
}
} catch (error) {
console.error(`Error loading options for ${dataSource}:`, error);
} finally {
// Reset lo stato di loading per questo dataSource
loadingOptionsFor[dataSource] = false;
}
});
// Aspetta che tutti i caricamenti siano completati
await Promise.all(loadingPromises);
};
</script>
<style scoped>
.input-container {
margin-bottom: 1em;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 10px;
}
.full-width-input {
width: 100%;
}
.editor ol {
list-style-type: decimal !important;
}
.editor ul {
list-style-type: disc !important;
}
pre {
white-space: pre-wrap; /* Fa andare a capo il contenuto automaticamente */
word-wrap: break-word; /* Interrompe le parole troppo lunghe */
overflow-wrap: break-word; /* Per compatibilità con più browser */
max-width: 100%; /* Imposta una larghezza massima pari al contenitore genitore */
overflow-x: auto; /* Aggiunge uno scorrimento orizzontale solo se necessario */
background-color: #f5f5f5; /* Colore di sfondo opzionale per migliorare leggibilità */
padding: 10px; /* Spaziatura interna */
border-radius: 5px; /* Bordo arrotondato opzionale */
font-family: monospace; /* Font specifico per codice */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Ombra per migliorare estetica */
}
.markdown-content {
word-wrap: break-word; /* Spezza le parole lunghe */
overflow-wrap: break-word; /* Per compatibilità con più browser */
max-width: 100%; /* Adatta il contenuto alla larghezza del contenitore */
overflow-x: auto; /* Aggiunge scorrimento orizzontale solo se necessario */
background-color: #f5f5f5; /* Sfondo per distinguere il contenuto */
}
</style>

View 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>

View File

@@ -1,173 +1,4 @@
<template>
<div v-if="loading" class="flex justify-center">
<ProgressSpinner style="width: 50px; height: 50px; margin-top: 50px" strokeWidth="3" fill="transparent"/>
</div>
<div v-else >
<h2 class="text-xl font-bold mt-6">Executions List</h2>
<DataTable v-model:filters="filters" v-model:expandedRows="expandedRows" @rowExpand="onRowExpand"
@rowCollapse="onRowCollapse" :value="scenario_execution_store.scenariosExecution"
:loading="loading_data"
:paginator="true"
:lazy="true"
:rows="scenario_execution_store.getPageSize"
:first="scenario_execution_store.getCurrentPage * scenario_execution_store.getPageSize"
:totalRecords="scenario_execution_store.getTotalRecords"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} records"
:rowsPerPageOptions="[10, 15, 20, 50, 100]" dataKey="id" :rowHover="true" rowGroupMode="subheader"
:sortOrder="1" filterDisplay="menu"
tableStyle="min-width: 70rem"
@page="onPage"
@sort="onSort"
removableSort>
<template #header>
<div class="flex justify-end">
<IconField>
<Button label="Clear Filters" @click="clearFilters" class="mr-2" />
<InputIcon>
<i class="pi pi-search" />
</InputIcon>
<InputText v-model="filters['_id'].constraints[0].value" placeholder="ID" />
<Button label="Apply" @click="fetchData(0, 10)" />
</IconField>
</div>
</template>
<Column field="scenario.name" header="Scenario Name" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false"
style="min-width: 12rem" >
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.scenario?.name }}
<i
class="pi pi-info-circle text-violet-600 cursor-pointer"
v-tooltip="slotProps.data?.scenario?.description || 'No description available'"
></i>
<!-- controllare il tooltip -->
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search by ScenarioName" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="execSharedMap.user_input.selected_application" header="Application Input" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false"
style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.execSharedMap?.user_input?.selected_application }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text"
placeholder="Search by Application" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<!-- <Column field="startDate"
filterField="startDate" header="Start Date" sortable
style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<Calendar
v-model="filterModel.value"
@input="(value) => {
filterModel.value = new Date(value); // Converte in oggetto Date
filterCallback();
}"
dateFormat="yy-mm-dd"
placeholder="Filter by Date"
/>
</template>
</Column> -->
<Column field="startDate" header="Start Date" filterField="startDate" dataType="date" style="min-width: 10rem" sortable>
<template #body="slotProps">
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
</template>
<!-- <template #filter="{ filterModel, filterCallback }">
<DatePicker v-model="filterModel.value" dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template> -->
</Column>
<Column field="scenario.aiModel.model" header="Model AI"
style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.scenario?.aiModel?.model }}
<i
class="pi pi-info-circle text-violet-600 cursor-pointer"
v-tooltip="'Provider: ' + slotProps.data?.scenario?.aiModel?.apiProvider + ' Token used: ' + slotProps.data?.usedTokens || 'No description available'"
></i>
</div>
</template>
<!-- <template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text"
placeholder="Search by Model" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template> -->
</Column>
<Column field="executedByUsername" header="Executed By" sortable
style="min-width: 12rem" :showApplyButton="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.executedByUsername || 'N/A' }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text"
placeholder="Search by Username" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="rating" header="Rating" sortable :showApplyButton="false" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
<template #body="slotProps">
<Rating :modelValue="slotProps.data.rating" :stars="5" :readonly="true" />
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search (1,2,3,4,5)" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="id" :style="{ position: 'sticky', right: '0', zIndex: '1', background: '#f3f3f3'}">
<template #body="slotProps">
<div class="flex justify-center items-center h-full">
<Button label="View" @click="goToScenarioExec(slotProps.data)" class="mt-0 ml-0" />
</div>
</template>
</Column>
<template #empty>
<tr>
<td :colspan="9" class="text-center">No execution found</td>
</tr>
</template>
</DataTable>
</div>
<div v-if="loading_data" class="flex justify-center">
<ProgressSpinner style="width: 30px; height: 30px; margin: 30px" strokeWidth="6" fill="transparent"/>
</div>
</template>
<script setup>
import { FilterMatchMode, FilterOperator } from '@primevue/core/api';
import 'md-editor-v3/lib/style.css';
import moment from 'moment';
@@ -191,7 +22,7 @@ const formData = ref({});
const exec_id = ref(null);
const exec_scenario = ref({});
const debug_modal = ref(false);
const execution_id = ref("");
const execution_id = ref('');
const listScenarios = ref([]);
const scenario_execution_store = ScenarioExecutionStore();
const toast = useToast();
@@ -202,43 +33,40 @@ const actualPageSize = ref(10);
const sortField = ref(null);
const sortOrder = ref(null);
const filters = ref({
'_id': { operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
},
_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'scenario.name': {
operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
},
'execSharedMap.user_input.selected_application': {
operator: FilterOperator.AND,
constraints: [{
value: userPrefStore.getSelApp?.fe_name || null, matchMode: FilterMatchMode.CONTAINS
}]
},
'execSharedMap.user_input.selected_application': {
operator: FilterOperator.AND,
constraints: [
{
value: userPrefStore.getSelApp?.fe_name || null,
matchMode: FilterMatchMode.CONTAINS
}
]
},
'scenario.aiModel.model': {
operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
},
},
'executedByUsername': {
executedByUsername: {
operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
},
'startDate': {
},
startDate: {
operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }]
},
},
'rating': {
rating: {
operator: FilterOperator.AND,
constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }]
}
}
});
onMounted(() => {
@@ -253,16 +81,13 @@ watch(() => route.params.name, updateFilters);
// fetchData(Math.floor(first.value / scenario_execution_store.getPageSize), scenario_execution_store.getPageSize, filters.value);
// });
function updateFilters() {
const selectedScenario = userPrefStore.getSelScenario;
if (selectedScenario && route.params.name!=='all') {
if (selectedScenario && route.params.name !== 'all') {
console.log('selectedScenario: im in');
filters.value['scenario.name'].constraints[0].value = selectedScenario;
}else{
} else {
filters.value['scenario.name'].constraints[0].value = null;
}
fetchData(0, 10);
@@ -275,11 +100,11 @@ function fetchDataWithFilters(filterCallback) {
function onSort(event) {
console.log('Sorting event:', event);
sortField.value = event.sortField;
sortOrder.value = event.sortOrder;
fetchData(0,actualPageSize.value);
fetchData(0, actualPageSize.value);
}
const fetchData = async (page, size) => {
@@ -298,12 +123,12 @@ const fetchData = async (page, size) => {
function clearFilters() {
filters.value = {
'_id': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'scenario.name': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'execSharedMap.user_input.selected_application': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'scenario.aiModel.model': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'executedByUsername': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
'startDate': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }
executedByUsername: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
startDate: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] }
};
fetchData(0, actualPageSize.value);
}
@@ -311,36 +136,545 @@ function clearFilters() {
const goToScenarioExec = (execScenarioItem) => {
console.log(execScenarioItem);
router.push({ name: 'scenario-exec-history', query: { id:execScenarioItem.id } });
router.push({ name: 'scenario-exec-history', query: { id: execScenarioItem.id } });
// scenario_execution_store.setSelectedExecScenario(execScenarioItem).then(() => {
// router.push({ name: 'scenario-exec-history', query: { id:execScenarioItem.id } });
// });
};
function onPage(event) {
function onPage(event) {
actualPage.value = event.page;
actualPageSize.value = event.rows;
fetchData(event.page, event.rows);
console.log('event onpage:', event);
//updateFilters();
}
}
// function onPage(event) {
// first.value = event.first; // Imposta la pagina corrente
// fetchData(Math.floor(first.value / scenario_execution_store.getPageSize), scenario_execution_store.getPageSize);
// }
</script>
<template>
<div class="exec-list-container">
<!-- Enhanced Header Section -->
<div class="header-section">
<div class="header-content">
<div class="header-icon">
<i class="pi pi-list" style="font-size: 1.36rem"></i>
</div>
<div class="header-text">
<h1 class="page-title">Scenario Executions</h1>
<p class="page-subtitle">View and manage all scenario execution history</p>
</div>
</div>
</div>
<div v-if="loading" class="loading-section">
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="3" fill="transparent" />
</div>
<div v-else class="table-section">
<DataTable
v-model:filters="filters"
v-model:expandedRows="expandedRows"
@rowExpand="onRowExpand"
@rowCollapse="onRowCollapse"
:value="scenario_execution_store.scenariosExecution"
:loading="loading_data"
:paginator="true"
:lazy="true"
:rows="scenario_execution_store.getPageSize"
:first="scenario_execution_store.getCurrentPage * scenario_execution_store.getPageSize"
:totalRecords="scenario_execution_store.getTotalRecords"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} records"
:rowsPerPageOptions="[10, 15, 20, 50, 100]"
dataKey="id"
:rowHover="true"
rowGroupMode="subheader"
:sortOrder="1"
filterDisplay="menu"
tableStyle="min-width: 70rem"
@page="onPage"
@sort="onSort"
removableSort
class="enhanced-table"
>
<template #header>
<div class="table-header">
<div class="table-header-content">
<i class="pi pi-filter"></i>
<h3 class="table-title">Filter Executions</h3>
</div>
<div class="filter-actions">
<IconField class="search-field">
<InputIcon>
<i class="pi pi-search" />
</InputIcon>
<InputText v-model="filters['_id'].constraints[0].value" placeholder="Search by ID" class="search-input" />
</IconField>
<Button label="Clear" @click="clearFilters" icon="pi pi-times" severity="secondary" outlined class="action-button" />
<Button label="Apply" @click="fetchData(0, 10)" icon="pi pi-check" severity="success" class="action-button" />
</div>
</div>
</template>
<Column field="scenario.name" header="Scenario Name" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false" style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.scenario?.name }}
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="slotProps.data?.scenario?.description || 'No description available'"></i>
<!-- controllare il tooltip -->
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search by ScenarioName" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="execSharedMap.user_input.selected_application" header="Application Input" sortable :showFilterOperator="false" :showApplyButton="false" :showAddButton="false" :showClearButton="false" style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.execSharedMap?.user_input?.selected_application }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search by Application" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<!-- <Column field="startDate"
filterField="startDate" header="Start Date" sortable
style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<Calendar
v-model="filterModel.value"
@input="(value) => {
filterModel.value = new Date(value); // Converte in oggetto Date
filterCallback();
}"
dateFormat="yy-mm-dd"
placeholder="Filter by Date"
/>
</template>
</Column> -->
<Column field="startDate" header="Start Date" filterField="startDate" dataType="date" style="min-width: 10rem" sortable>
<template #body="slotProps">
{{ moment(slotProps.data.startDate).format('DD-MM-YYYY HH:mm:ss') }}
</template>
<!-- <template #filter="{ filterModel, filterCallback }">
<DatePicker v-model="filterModel.value" dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template> -->
</Column>
<Column field="scenario.aiModel.model" header="Model AI" style="min-width: 12rem">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.scenario?.aiModel?.model }}
<i class="pi pi-info-circle text-violet-600 cursor-pointer" v-tooltip="'Provider: ' + slotProps.data?.scenario?.aiModel?.apiProvider + ' Token used: ' + slotProps.data?.usedTokens || 'No description available'"></i>
</div>
</template>
<!-- <template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text"
placeholder="Search by Model" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template> -->
</Column>
<Column field="executedByUsername" header="Executed By" sortable style="min-width: 12rem" :showApplyButton="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
<template #body="slotProps">
<div class="flex items-center gap-2">
{{ slotProps.data.executedByUsername || 'N/A' }}
</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search by Username" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="rating" header="Rating" sortable :showApplyButton="false" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false" :showClearButton="false">
<template #body="slotProps">
<Rating :modelValue="slotProps.data.rating" :stars="5" :readonly="true" />
</template>
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" type="text" placeholder="Search (1,2,3,4,5)" />
<Button label="Apply" @click="fetchDataWithFilters(filterCallback)" />
</template>
</Column>
<Column field="id" :style="{ position: 'sticky', right: '0', zIndex: '1', background: '#f3f3f3' }">
<template #body="slotProps">
<div class="flex justify-center items-center h-full">
<Button label="View" @click="goToScenarioExec(slotProps.data)" class="mt-0 ml-0" />
</div>
</template>
</Column>
<template #empty>
<div class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p class="empty-text">No executions found</p>
<p class="empty-subtext">Try adjusting your filters or execute a new scenario</p>
</div>
</template>
</DataTable>
<div v-if="loading_data" class="table-loading">
<ProgressSpinner style="width: 30px; height: 30px" strokeWidth="6" fill="transparent" />
</div>
</div>
</div>
</template>
<style scoped>
/* Container */
.exec-list-container {
max-width: 1400px;
margin: 0 auto;
padding: 1.5rem;
}
/* Enhanced Header Section */
.header-section {
margin-bottom: 2rem;
animation: fadeInDown 0.5s ease-out;
}
.header-content {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 1.36rem;
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
color: white;
}
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10px);
}
.header-text {
flex: 1;
}
.page-title {
font-size: 1.8rem !important;
font-weight: 700 !important;
margin: 0 0 0.5rem 0 !important;
color: white !important;
}
.page-subtitle {
font-size: 1.1rem;
margin: 0;
opacity: 0.95;
line-height: 1.6;
}
/* Loading Section */
.loading-section {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
animation: fadeIn 0.5s ease-out;
}
/* Table Section */
.table-section {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
animation: fadeInUp 0.5s ease-out;
}
.enhanced-table {
border-radius: 16px;
}
.enhanced-table :deep(.p-datatable-header) {
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
border-bottom: 2px solid #a100ff;
padding: 1.5rem 2rem;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.table-header-content {
display: flex;
align-items: center;
gap: 0.75rem;
color: #a100ff;
}
.table-header-content i {
font-size: 1.4rem;
}
.table-title {
font-size: 1.2rem;
font-weight: 700;
margin: 0;
color: #a100ff;
}
.filter-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.search-field {
min-width: 250px;
}
.search-input :deep(.p-inputtext) {
border-radius: 8px;
border: 1px solid #cbd5e0;
transition: all 0.3s ease;
}
.search-input :deep(.p-inputtext:hover) {
border-color: #a100ff;
}
.search-input :deep(.p-inputtext:focus) {
border-color: #a100ff;
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
}
.action-button {
min-width: 120px;
font-weight: 600;
transition: all 0.3s ease;
}
.action-button:deep(button) {
justify-content: center;
}
.action-button:not(:disabled):hover {
transform: translateY(-2px);
}
/* Table Styling */
.enhanced-table :deep(.p-datatable-thead > tr > th) {
background: #f8f9fa;
color: #2d3748;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
padding: 1rem;
}
.enhanced-table :deep(.p-datatable-tbody > tr) {
transition: all 0.2s ease;
border-bottom: 1px solid #e2e8f0;
}
.enhanced-table :deep(.p-datatable-tbody > tr:hover) {
background: #f7fafc;
transform: translateX(3px);
}
.enhanced-table :deep(.p-datatable-tbody > tr > td) {
padding: 1rem;
vertical-align: middle;
}
/* Filter Column Templates */
.enhanced-table :deep(.p-column-filter-overlay) {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.enhanced-table :deep(.p-column-filter-overlay .p-inputtext) {
border-radius: 8px;
border: 1px solid #cbd5e0;
margin-bottom: 0.5rem;
}
/* Info Icons */
.enhanced-table :deep(.pi-info-circle) {
color: #a100ff;
transition: all 0.2s ease;
}
.enhanced-table :deep(.pi-info-circle:hover) {
transform: scale(1.2);
color: #5568d3;
}
/* Rating Component */
.enhanced-table :deep(.p-rating) {
gap: 0.25rem;
}
.enhanced-table :deep(.p-rating .p-rating-icon) {
color: #a100ff;
}
/* View Button */
.enhanced-table :deep(.p-button) {
min-width: 100px;
font-weight: 600;
transition: all 0.3s ease;
}
.enhanced-table :deep(.p-button:hover) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(161, 0, 255, 0.3);
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: #a0aec0;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-text {
font-size: 1.2rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: #718096;
}
.empty-subtext {
font-size: 1rem;
margin: 0;
color: #a0aec0;
}
/* Table Loading */
.table-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
/* Sticky Column Styling */
.enhanced-table :deep(td[style*='position: sticky']) {
background: linear-gradient(to left, #ffffff 0%, #ffffff 90%, transparent 100%) !important;
box-shadow: -4px 0 8px rgba(0, 0, 0, 0.05);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.exec-list-container {
padding: 1rem;
}
.header-content {
flex-direction: column;
text-align: center;
padding: 1.5rem;
}
.scenario-title {
font-size: 1.4rem !important;
}
.scenario-description {
font-size: 1rem;
}
.table-header {
flex-direction: column;
align-items: stretch;
}
.filter-actions {
width: 100%;
}
.search-field {
width: 100%;
min-width: auto;
}
.action-button {
flex: 1;
}
}
/* Legacy Styles for Compatibility */
.input-container {
margin-bottom: 1em;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.5em;
align-items: center;
gap: 0.5em;
margin-top: 10px;
}
@@ -348,14 +682,11 @@ const goToScenarioExec = (execScenarioItem) => {
width: 100%;
}
.editor ol {
list-style-type: decimal !important;
list-style-type: decimal !important;
}
.editor ul {
list-style-type: disc !important;
list-style-type: disc !important;
}
</style>
</style>

View File

@@ -1,139 +1,226 @@
<template>
<div>
<h1>Available Scenarios</h1>
</div>
<div >
<DataView :value="scenario_store.filteredScenarios" :layout="layout" paginator :rows="8">
<template #header>
<div class="header-container">
<div class="search-bar">
<i class="pi pi-search search-icon"></i>
<InputText
class="search-input"
type="search"
placeholder="Search"
v-model="scenario_store.filterString"
size="medium"
variant="filled"
style="border: 1px solid #a100ff"
/>
</div>
<div class="card flex justify-center">
<SelectButton v-model="scenario_store.typeFilter" :options="scenarioTypeOp" optionLabel="name" />
</div>
<SelectButton v-model="layout" :options="options" :allowEmpty="false" class="layout-switch">
<template #option="{ option }">
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-table']" />
</template>
</SelectButton>
</div>
</template>
<template #list="slotProps">
<div class="flex flex-col space-y-4 mt-2">
<div v-for="(item, index) in slotProps.items" :key="index">
<div class="flex flex-col sm:flex-row sm:items-center p-6 gap-4 bg-white dark:bg-gray-800 rounded-lg shadow-md"
:class="{ 'border-t border-gray-200 dark:border-gray-700': index !== 0 }">
<div class="flex flex-col flex-grow">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</h3>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-2">{{ item.description }}</p>
</div>
<div class="mt-auto flex justify-end">
<Button @click="executeScenario(item.id)" label="Load" class="flex-auto md:flex-initial text-white">
<ChevronRightIcon class="w-5 h-10 text-white transition-transform transform hover:translate-x-1"/>
</Button>
</div>
</div>
</div>
</div>
</template>
<template #grid="slotProps">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mt-2">
<div v-for="(item, index) in slotProps.items" :key="index" class="p-2">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md flex flex-col h-full">
<div class="p-4 flex flex-col flex-grow">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</h3>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-2">{{ item.description }}</p>
</div>
<div class="p-2 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center w-full">
<div v-if="item.visible==='DRAFT'" class="text-xs font-semibold text-white bg-purple-400 px-2 py-1 inline-flex items-center justify-center w-auto">
{{ item.visible }}
</div>
<Button @click="executeScenario(item.id)" size="small" label="Load" class="ml-auto flex-initial text-white">
<ChevronRightIcon class="w-6 h-5 text-white transition-transform transform hover:translate-x-1"/>
</Button>
</div>
</div>
</div>
</div>
</template>
</DataView>
</div>
</template>
<script setup>
import { ChevronRightIcon } from '@heroicons/vue/24/solid';
import DataView from 'primevue/dataview';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { ScenarioStore } from '../../stores/ScenarioStore.js';
import { UserPrefStore } from '../../stores/UserPrefStore.js';
const router = useRouter()
const layout = ref('grid');
const options = ref(['list', 'grid']);
const router = useRouter();
const layout = ref('grid');
const options = ref(['list', 'grid']);
const scenario_store = ScenarioStore();
const userPrefStore = UserPrefStore();
const scenario_store = ScenarioStore();
const userPrefStore = UserPrefStore();
const scenarioTypeOp = ref([
{ name: 'All', value: 'all' },
//{ name: 'Cross', value: 'cross' },
{ name: 'Project', value: 'project' },
{ name: 'Application', value: 'application' }
]);
onMounted(() => {
scenario_store.setFilterString('');
userPrefStore.fetchUserData().then(() => {
scenario_store.fetchScenarios();
if (userPrefStore.getSelApp != null) {
scenario_store.fetchApplicationScenarios();
}
});
});
onMounted(() => {
scenario_store.setFilterString('');
userPrefStore.fetchUserData().then(() => {
//scenario_store.fetchScenariosCross();
scenario_store.fetchScenarios();
if(userPrefStore.getSelApp != null){
scenario_store.fetchApplicationScenarios();
}
});
});
const executeScenario = (id) => {
router.push({ name: 'scenario-exec', params: { id: id } });
}
const executeScenario = (id) => {
router.push({ name: 'scenario-exec', params: { id: id } });
};
</script>
<template>
<div class="scenario-list-container">
<!-- Enhanced Header Section -->
<div class="header-section">
<div class="header-content">
<div class="header-icon">
<i class="pi pi-list" style="font-size: 1.36rem"></i>
</div>
<div class="header-text">
<h1 class="page-title">Available Scenarios</h1>
<p class="page-subtitle">Browse and execute scenarios tailored for your project</p>
</div>
</div>
</div>
<!-- Enhanced DataView Section -->
<div class="dataview-section">
<DataView :value="scenario_store.filteredScenarios" :layout="layout" paginator :rows="8" class="custom-dataview">
<template #header>
<div class="dataview-header">
<!-- Search Bar -->
<div class="search-container">
<i class="pi pi-search search-icon"></i>
<InputText class="search-input" type="search" placeholder="Search scenarios..." v-model="scenario_store.filterString" />
</div>
<!-- Type Filter -->
<div class="filter-container">
<SelectButton v-model="scenario_store.typeFilter" :options="scenarioTypeOp" optionLabel="name" class="type-filter" />
</div>
<!-- Layout Switch -->
<SelectButton v-model="layout" :options="options" :allowEmpty="false" class="layout-switch">
<template #option="{ option }">
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-th-large']" />
</template>
</SelectButton>
</div>
</template>
<template #list="slotProps">
<div class="list-view">
<div v-for="(item, index) in slotProps.items" :key="index" class="list-item">
<div class="list-item-content">
<div class="item-info">
<div class="item-header">
<i class="pi pi-bolt item-icon"></i>
<h3 class="item-title">{{ item.name }}</h3>
<span v-if="item.visible === 'DRAFT'" class="draft-badge">DRAFT</span>
</div>
<p class="item-description">{{ item.description }}</p>
</div>
<div class="item-actions">
<Button @click="executeScenario(item.id)" label="Load Scenario" severity="success" icon="pi pi-arrow-right" iconPos="right" class="load-button"></Button>
</div>
</div>
</div>
</div>
</template>
<template #grid="slotProps">
<div class="grid-view">
<div v-for="(item, index) in slotProps.items" :key="index" class="grid-item">
<div class="grid-item-card">
<div class="card-header">
<div class="card-header-title">
<i class="pi pi-bolt card-icon"></i>
<h3 class="card-title">{{ item.name }}</h3>
</div>
<span v-if="item.visible === 'DRAFT'" class="draft-badge-small">DRAFT</span>
</div>
<div class="card-content">
<p class="card-description">{{ item.description }}</p>
</div>
<div class="card-footer">
<Button @click="executeScenario(item.id)" label="Load" severity="success" icon="pi pi-arrow-right" iconPos="right" size="small" class="card-button"></Button>
</div>
</div>
</div>
</div>
</template>
</DataView>
</div>
</div>
</template>
<style scoped>
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
/* Container */
.scenario-list-container {
max-width: 1400px;
margin: 0 auto;
padding: 1.5rem;
}
.search-bar {
/* Enhanced Header Section */
.header-section {
margin-bottom: 2rem;
animation: fadeInDown 0.5s ease-out;
}
.header-content {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 1.36rem;
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
color: white;
}
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10px);
}
.header-text {
flex: 1;
}
.page-title {
font-size: 1.8rem !important;
font-weight: 700 !important;
margin: 0 0 0.5rem 0 !important;
color: white !important;
}
.page-subtitle {
font-size: 1.1rem;
margin: 0;
opacity: 0.95;
line-height: 1.6;
}
/* DataView Section */
.dataview-section {
animation: fadeIn 0.5s ease-out;
}
.custom-dataview {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.dataview-header {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
padding: 1.5rem;
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
border-bottom: 2px solid #a100ff;
}
/* Search Container */
.search-container {
flex: 1;
min-width: 250px;
display: flex;
align-items: center;
background: white;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 0.75rem 1rem;
transition: all 0.3s ease;
}
.search-container:hover {
border-color: #a100ff;
}
.search-container:focus-within {
border-color: #a100ff;
box-shadow: 0 0 0 3px rgba(161, 0, 255, 0.1);
}
.search-icon {
color:#334155;
margin-right: 5px;
color: #a100ff;
margin-right: 0.75rem;
font-size: 1.1rem;
}
.search-input {
@@ -141,6 +228,8 @@ const scenarioTypeOp = ref([
outline: none;
box-shadow: none;
flex: 1;
font-size: 0.95rem;
background: transparent;
}
.search-input:focus {
@@ -149,4 +238,301 @@ const scenarioTypeOp = ref([
outline: none;
}
</style>
/* Filter Container */
.filter-container {
display: flex;
align-items: center;
}
.type-filter :deep(.p-button) {
border-radius: 8px;
transition: all 0.3s ease;
}
.type-filter :deep(.p-button:hover) {
transform: translateY(-2px);
}
/* Layout Switch */
.layout-switch :deep(.p-button) {
border-radius: 8px;
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.layout-switch :deep(.p-button i) {
font-size: 1.2rem;
}
/* List View */
.list-view {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.list-item {
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
overflow: hidden;
}
.list-item:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(161, 0, 255, 0.15);
border-color: #a100ff;
}
.list-item-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
gap: 2rem;
}
.item-info {
flex: 1;
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.item-icon {
color: #a100ff;
font-size: 1.3rem;
}
.item-title {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.draft-badge {
padding: 0.25rem 0.75rem;
background: #a100ff;
color: white;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.item-description {
color: #64748b;
font-size: 0.95rem;
line-height: 1.6;
margin: 0;
}
.item-actions {
display: flex;
align-items: center;
}
.load-button :deep(.p-button) {
min-width: 150px;
}
/* Grid View */
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1.5rem;
}
.grid-item-card {
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e2e8f0;
height: 100%;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
overflow: hidden;
}
.grid-item-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(161, 0, 255, 0.2);
border-color: #a100ff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem;
background: linear-gradient(135deg, #a100ff15 0%, #7b00cc15 100%);
border-bottom: 1px solid #e2e8f0;
}
.card-header-title {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.card-icon {
color: #a100ff;
font-size: 1.3rem;
flex-shrink: 0;
}
.draft-badge-small {
padding: 0.25rem 0.5rem;
background: #a100ff;
color: white;
border-radius: 8px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
flex-shrink: 0;
}
.card-content {
flex: 1;
padding: 1.25rem;
}
.card-title {
font-size: 1.05rem;
font-weight: 600;
color: #2d3748;
margin: 0;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-description {
color: #64748b;
font-size: 0.9rem;
line-height: 1.5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
padding: 1rem 1.25rem;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
}
.card-button {
width: 100%;
justify-content: center;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.scenario-list-container {
padding: 1rem;
}
.header-content {
flex-direction: column;
text-align: center;
padding: 1.5rem;
}
.page-title {
font-size: 1.4rem !important;
}
.page-subtitle {
font-size: 1rem;
}
.dataview-header {
flex-direction: column;
align-items: stretch;
}
.search-container {
min-width: 100%;
}
.list-item-content {
flex-direction: column;
gap: 1rem;
}
.item-actions {
width: 100%;
}
.load-button {
width: 100%;
}
.grid-view {
grid-template-columns: 1fr;
}
}
/* DataView Override Styles */
.custom-dataview :deep(.p-dataview-content) {
background: transparent;
}
.custom-dataview :deep(.p-paginator) {
background: white;
border-top: 2px solid #e2e8f0;
padding: 1rem 1.5rem;
}
.custom-dataview :deep(.p-paginator .p-paginator-pages .p-paginator-page.p-highlight) {
background: #a100ff;
border-color: #a100ff;
}
.custom-dataview :deep(.p-paginator .p-paginator-pages .p-paginator-page:hover) {
background: #a100ff15;
border-color: #a100ff;
}
</style>

View File

@@ -1,44 +1,150 @@
<template>
<div class="flex items-center justify-between p-1">
<h1 class="flex items-center">
<i class="pi pi-comments mr-2"></i>
<span>Chat with WizardAI</span>
</h1>
</div>
<div class="flex items-center justify-between p-1">
<h2>
<span>
Contextualized on
</span><br/>
<span>
Project: <strong>{{ userPrefStore.user.selectedProject.fe_name }}</strong>
</span><br/>
<span v-if="userPrefStore.user.selectedApplication">
Application: <strong>{{ userPrefStore.user.selectedApplication.fe_name}}</strong>
</span>
</h2>
</div>
<div className="card">
<ChatClient />
</div>
</template>
<script setup>
import ChatClient from '@/components/ChatClient.vue';
import { UserPrefStore } from '@/stores/UserPrefStore.js';
import { onMounted, computed, watch, ref} from 'vue';
import { onMounted } from 'vue';
const userPrefStore = UserPrefStore();
onMounted(() => {
console.log('userPrefStore', userPrefStore);
});
</script>
<style >
<template>
<div class="chat-page-container">
<!-- Enhanced Header Section -->
<div class="header-section">
<div class="header-content">
<div class="header-icon">
<i class="pi pi-comments" style="font-size: 1.36rem"></i>
</div>
<div class="header-text">
<h1 class="page-title">Chat with WizardAI</h1>
<p class="page-subtitle">
Contextualized on Project: <strong>{{ userPrefStore.user.selectedProject.fe_name }}</strong>
<span v-if="userPrefStore.user.selectedApplication">
Application: <strong>{{ userPrefStore.user.selectedApplication.fe_name }}</strong>
</span>
</p>
</div>
</div>
</div>
</style>
<!-- Chat Section -->
<div class="chat-section">
<div class="chat-card">
<ChatClient />
</div>
</div>
</div>
</template>
<style scoped>
.chat-page-container {
max-width: 1400px;
margin: 0 auto;
padding: 1.5rem;
}
/* Enhanced Header Section */
.header-section {
margin-bottom: 2rem;
animation: fadeInDown 0.5s ease-out;
}
.header-content {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 1.36rem;
background: linear-gradient(135deg, #a100ff 0%, #7b00cc 100%);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(161, 0, 255, 0.3);
color: white;
}
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10px);
}
.header-text {
flex: 1;
}
.page-title {
font-size: 1.8rem !important;
font-weight: 700 !important;
margin: 0 0 0.5rem 0 !important;
color: white !important;
}
.page-subtitle {
font-size: 1.1rem;
margin: 0;
opacity: 0.95;
line-height: 1.6;
}
/* Chat Section */
.chat-section {
animation: fadeIn 0.5s ease-out;
}
.chat-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.chat-page-container {
padding: 1rem;
}
.header-content {
flex-direction: column;
text-align: center;
padding: 1.5rem;
}
.page-title {
font-size: 1.4rem !important;
}
.page-subtitle {
font-size: 1rem;
}
}
</style>