Files
hermione-fe/src/components/ChatClient.vue
Andrea Terzani af8a8b67c3 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.
2025-12-12 19:28:17 +01:00

565 lines
16 KiB
Vue

<script>
import { marked } from 'marked';
import Button from 'primevue/button';
import Card from 'primevue/card';
import Checkbox from 'primevue/checkbox';
import InputGroup from 'primevue/inputgroup';
import InputGroupAddon from 'primevue/inputgroupaddon';
import InputText from 'primevue/inputtext';
import ScrollPanel from 'primevue/scrollpanel';
import MarkdownViewer from './MarkdownViewer.vue';
import { UserPrefStore } from '../stores/UserPrefStore.js';
const userPrefStore = UserPrefStore();
export default {
name: 'ChatGPTInterface',
components: {
'p-scrollPanel': ScrollPanel,
'p-inputText': InputText,
'p-button': Button,
'p-checkbox': Checkbox,
'p-card': Card,
'p-inputGroup': InputGroup,
'p-inputGroupAddon': InputGroupAddon,
MarkdownViewer
},
props: {
scenarioExecutionId: {
type: String,
default: ''
}
},
data() {
return {
conversationId: `${userPrefStore.user.id}-${userPrefStore.user.selectedProject.internal_name}`,
message: '',
messages: [],
useDocumentation: true,
useSource: true,
project: userPrefStore.user.selectedProject.internal_name,
application: userPrefStore.getSelApp ? userPrefStore.getSelApp.internal_name : '',
scenarioExecutionId: this.scenarioExecutionId,
showSettings: false,
authorization: 'Bearer ' + this.$auth.token(),
waitingData: false,
previousMessagesLength: 0
};
},
//
mounted() {
console.log('userPrefStore', userPrefStore);
this.updateConversationId();
},
methods: {
async updateConversationId() {
this.conversationId = this.scenarioExecutionId ? `${userPrefStore.user.id}-${this.scenarioExecutionId}` : `${userPrefStore.user.id}-${userPrefStore.user.selectedProject.internal_name}`;
await this.fetchChatHistory();
if (this.scenarioExecutionId && this.messages.length === 0) {
this.loadContext();
}
},
async fetchChatHistory() {
if (!this.conversationId.trim()) {
console.warn('No conversation ID set.');
return;
}
try {
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL.replace('/hermione', '')}/chatservice/get-history?conversationId=${this.conversationId}&lastN=100`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', authorization: 'Bearer ' + this.$auth.token() }
});
if (!response.ok) throw new Error('Failed to fetch chat history');
const history = await response.json();
// Convert API format to frontend format
this.messages = [];
history.forEach((msg) => {
if (msg.messageType != 'SYSTEM') {
this.messages.push({
sender: msg.messageType === 'USER' ? 'user' : 'bot',
text: msg.text
});
}
});
this.scrollToBottom();
} catch (error) {
console.error('Error loading chat history:', error);
}
},
async sendMessage() {
if (this.message.trim() === '' || !this.conversationId.trim()) return;
this.messages.push({ sender: 'user', text: this.message });
const botMessage = { sender: 'bot', text: '' };
this.messages.push(botMessage);
this.scrollToBottom();
const payload = {
message: this.message,
conversationId: this.conversationId,
useDocumentation: this.useDocumentation,
useSource: this.useSource,
project: this.project,
application: this.application
};
this.message = '';
try {
this.waitingData = true;
const response = await fetch(import.meta.env.VITE_BACKEND_URL.replace('/hermione', '') + '/chatservice/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', authorization: this.authorization },
body: JSON.stringify(payload)
});
this.waitingData = false;
if (!response.body) throw new Error('Streaming not supported');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const processStream = async ({ done, value }) => {
if (done) return;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n');
buffer = parts.pop();
parts.forEach((line) => {
if (line.startsWith('data:')) {
try {
const jsonData = JSON.parse(line.slice(5).trim());
if (jsonData.result?.output?.text) {
botMessage.text += jsonData.result.output.text;
}
this.$forceUpdate();
this.scrollToBottom();
} catch (error) {
console.error('Error parsing JSON:', error);
}
}
});
return reader.read().then(processStream);
};
await reader.read().then(processStream);
} catch (error) {
console.error('Error fetching response:', error);
botMessage.text += '\n[Error fetching response]';
}
},
formatMessage(text) {
return marked.parse(text); // Converts Markdown to HTML
},
scrollToBottom() {
this.$nextTick(() => {
const container = this.$refs.messagesContainer;
container.scrollTop = container.scrollHeight;
});
},
resetChat() {
this.messages = []; // Clear chat messages
this.fetchChatHistory(); // Reload history for new conversation ID
},
async clearHistory() {
try {
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL.replace('/hermione', '')}/chatservice/delete-history?conversationId=${this.conversationId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', authorization: this.authorization }
});
if (!response.ok) {
throw new Error('Failed to clear chat history');
}
this.messages = [];
console.log('Chat history deleted successfully!');
} catch (error) {
console.error('Error clearing chat history:', error);
}
},
async loadContext() {
try {
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL.replace('/hermione', '')}/chatservice/load-context-to-conversation?conversationId=${this.conversationId}&scenarioExecutionId=${this.scenarioExecutionId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', authorization: this.authorization }
});
if (!response.ok) {
throw new Error('Failed to clear chat history');
}
this.messages = [];
this.resetChat();
console.log('Chat history deleted successfully!');
} catch (error) {
console.error('Error clearing chat history:', error);
}
}
}
};
</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 'md-editor-v3/lib/style.css';
/* Container */
.chat-wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 600px;
max-height: 75vh;
}
/* 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;
}
/* 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: 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;
}
.chat-message.bot .message-bubble {
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);
}
.chat-message.user .message-bubble {
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;
}
/* Message Content */
.bot-message-content {
overflow-wrap: break-word;
word-wrap: break-word;
max-width: 100%;
}
.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>