Files
eveAI/eveai_chat_client/static/assets/vue-components/ChatApp.vue
Josako 5e81595622 Changes for eveai_chat_client:
- Modal display of privacy statement & Terms & Conditions
- Consent-flag ==> check of privacy and Terms & Conditions
- customisation option added to show or hide DynamicForm titles
2025-07-28 21:47:56 +02:00

574 lines
20 KiB
Vue

active_text_color<template>
<div class="chat-app-container">
<!-- Message History - takes available space -->
<message-history
:messages="displayMessages"
:is-typing="isTyping"
:is-submitting-form="isSubmittingForm"
:api-prefix="apiPrefix"
:auto-scroll="true"
@specialist-error="handleSpecialistError"
@specialist-complete="handleSpecialistComplete"
ref="messageHistory"
class="chat-messages-area"
></message-history>
<!-- Chat Input - to the bottom -->
<chat-input
:current-message="currentMessage"
:is-loading="isLoading"
:max-length="2000"
:allow-file-upload="true"
:allow-voice-message="false"
:form-data="currentInputFormData"
:active-ai-message="activeAiMessage"
:api-prefix="apiPrefix"
@send-message="sendMessage"
@update-message="updateCurrentMessage"
@upload-file="handleFileUpload"
@record-voice="handleVoiceRecord"
@submit-form="submitFormFromInput"
@specialist-error="handleSpecialistError"
@specialist-complete="handleSpecialistComplete"
ref="chatInput"
class="chat-input-area"
></chat-input>
<!-- Content Modal - positioned at ChatApp level -->
<content-modal
:show="contentModal.modalState.show"
:title="contentModal.modalState.title"
:content="contentModal.modalState.content"
:version="contentModal.modalState.version"
:loading="contentModal.modalState.loading"
:error="contentModal.modalState.error"
:error-message="contentModal.modalState.errorMessage"
@close="contentModal.hideModal"
@retry="contentModal.retryLoad"
/>
</div>
</template>
<script>
// Import all components as Vue SFCs
import TypingIndicator from './TypingIndicator.vue';
import FormField from './FormField.vue';
import DynamicForm from './DynamicForm.vue';
import ChatMessage from './ChatMessage.vue';
import MessageHistory from './MessageHistory.vue';
import ProgressTracker from './ProgressTracker.vue';
import LanguageSelector from './LanguageSelector.vue';
import ChatInput from './ChatInput.vue';
import ContentModal from './ContentModal.vue';
// Import language provider
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
// Import content modal composable
import { provideContentModal } from '../js/composables/useContentModal.js';
import { provide } from 'vue';
export default {
name: 'ChatApp',
components: {
TypingIndicator,
FormField,
DynamicForm,
ChatMessage,
MessageHistory,
ProgressTracker,
ChatInput,
ContentModal
},
setup() {
// Haal initiële taal uit chatConfig
const initialLanguage = window.chatConfig?.language || 'nl';
const apiPrefix = window.chatConfig?.apiPrefix || '';
// Creëer language provider
const languageProvider = createLanguageProvider(initialLanguage, apiPrefix);
// Creëer en provide content modal
const contentModal = provideContentModal();
// Provide aan alle child components
provide(LANGUAGE_PROVIDER_KEY, languageProvider);
return {
languageProvider,
contentModal
};
},
data() {
// Maak een lokale kopie van de chatConfig om undefined errors te voorkomen
const chatConfig = window.chatConfig || {};
const settings = chatConfig.settings || {};
const initialLanguage = chatConfig.language || 'en';
const originalExplanation = chatConfig.explanation || '';
const tenantMake = chatConfig.tenantMake || {};
return {
// Tenant info
tenantName: tenantMake.name || 'EveAI',
tenantLogoUrl: tenantMake.logo_url || '',
// Taal gerelateerde data
currentLanguage: initialLanguage,
supportedLanguageDetails: chatConfig.supportedLanguageDetails || {},
allowedLanguages: chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
supportedLanguages: chatConfig.supportedLanguages || [],
originalExplanation: originalExplanation,
explanation: chatConfig.explanation || '',
// Chat-specific data
currentMessage: '',
allMessages: [],
isTyping: false,
isLoading: false,
isSubmittingForm: false,
messageIdCounter: 1,
formValues: {},
currentInputFormData: null,
// API prefix voor endpoints
apiPrefix: chatConfig.apiPrefix || '',
// Configuration from Flask/server
conversationId: chatConfig.conversationId || 'default',
userId: chatConfig.userId || null,
userName: chatConfig.userName || '',
// Settings met standaard waarden en overschreven door server config
settings: {
maxMessageLength: settings.maxMessageLength || 2000,
allowFileUpload: settings.allowFileUpload === true,
allowVoiceMessage: settings.allowVoiceMessage === true,
autoScroll: settings.autoScroll === true
},
// UI state
isMobile: window.innerWidth <= 768,
showSidebar: window.innerWidth > 768,
// Advanced features
messageSearch: '',
filteredMessages: [],
isSearching: false
};
},
computed: {
// Keep existing computed from base.html
compiledExplanation() {
if (typeof marked === 'function') {
return marked(this.explanation);
} else if (marked && typeof marked.parse === 'function') {
return marked.parse(this.explanation.replace(/\[\[(.*?)\]\]/g, '<strong>$1</strong>'));
} else {
console.error('Marked library not properly loaded');
return this.explanation;
}
},
displayMessages() {
return this.isSearching ? this.filteredMessages : this.allMessages;
},
// Active AI message that should be shown in ChatInput
activeAiMessage() {
return this.allMessages.find(msg => msg.isTemporarilyAtBottom);
},
hasMessages() {
return this.allMessages.length > 0;
},
displayLanguages() {
// Filter de ondersteunde talen op basis van de toegestane talen
if (!this.supportedLanguages || !this.allowedLanguages) {
return [];
}
return this.supportedLanguages.filter(lang =>
this.allowedLanguages.includes(lang.code)
);
}
},
mounted() {
this.initializeChat();
this.setupEventListeners();
},
beforeUnmount() {
this.cleanup();
},
methods: {
// Initialization
initializeChat() {
console.log('Initializing chat application...');
// Load historical messages from server
this.loadHistoricalMessages();
console.log('Nr of messages:', this.allMessages.length);
// Add welcome message if no history
if (this.allMessages.length === 0) {
this.addWelcomeMessage();
}
// Focus input after initialization
this.$nextTick(() => {
this.focusChatInput();
});
},
loadHistoricalMessages() {
// Veilige toegang tot messages met fallback
const chatConfig = window.chatConfig || {};
const historicalMessages = chatConfig.messages || [];
if (historicalMessages.length > 0) {
this.allMessages = historicalMessages
.filter(msg => msg !== null && msg !== undefined) // Filter null/undefined berichten uit
.map(msg => {
// Zorg voor een correct geformatteerde bericht-object
return {
id: this.messageIdCounter++,
content: typeof msg === 'string' ? msg : (msg.content || ''),
sender: msg.sender || 'ai',
type: msg.type || 'text',
timestamp: msg.timestamp || new Date().toISOString(),
formData: msg.formData || null,
status: msg.status || 'delivered'
};
});
console.log(`Loaded ${this.allMessages.length} historical messages`);
}
},
async addWelcomeMessage() {
console.log('Sending initialize message to backend');
// Toon typing indicator
this.isTyping = true;
this.isLoading = true;
console.log('API prefix:', this.apiPrefix);
try {
const response = await fetch(`${this.apiPrefix}/api/send_message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'Initialize',
language: this.currentLanguage,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Initialize response:', data);
if (data.task_id) {
// Add a placeholder message that will be updated by the progress tracker
const placeholderMessage = {
id: this.messageIdCounter++,
content: 'Bezig met laden...',
sender: 'ai',
type: 'text',
timestamp: new Date().toISOString(),
taskId: data.task_id,
status: 'processing',
isTemporarilyAtBottom: true
};
this.allMessages.push(placeholderMessage);
}
} catch (error) {
console.error('Error sending initialize message:', error);
this.addMessage({
content: 'Er is een fout opgetreden bij het initialiseren van de chat.',
sender: 'ai',
type: 'error'
});
} finally {
this.isTyping = false;
this.isLoading = false;
}
},
// Message repositioning logic - simplified to only toggle flag
repositionLatestAiMessage() {
// Find AI message with isTemporarilyAtBottom flag
const aiMessage = this.allMessages.find(msg =>
msg.sender === 'ai' && msg.taskId && msg.isTemporarilyAtBottom
);
if (aiMessage) {
// Simply turn off the flag - no array manipulation needed
aiMessage.isTemporarilyAtBottom = false;
console.log('AI message returned to normal flow position');
}
},
// Message handling
addMessage(messageData) {
const message = {
id: this.messageIdCounter++,
content: messageData.content || '',
sender: messageData.sender || 'user',
type: messageData.type || 'text',
timestamp: messageData.timestamp || new Date().toISOString(),
formData: messageData.formData || null,
formValues: messageData.formValues || null,
status: messageData.status || 'delivered'
};
this.allMessages.push(message);
// Auto-scroll to bottom
this.$nextTick(() => {
this.scrollToBottom();
});
return message;
},
async sendMessage() {
if (!this.currentMessage.trim() && !this.currentInputFormData) {
return;
}
console.log('Sending message:', this.currentMessage);
// FIRST: Reposition latest AI message to correct chronological place
this.repositionLatestAiMessage();
// THEN: Add user message to chat
const userMessage = this.addMessage({
content: this.currentMessage,
sender: 'user',
formData: this.currentInputFormData,
formValues: this.formValues
});
// Clear input
const messageToSend = this.currentMessage;
const formValuesToSend = { ...this.formValues };
this.currentMessage = '';
this.formValues = {};
this.currentInputFormData = null;
// Show typing indicator
this.isTyping = true;
this.isLoading = true;
try {
const response = await fetch(`${this.apiPrefix}/api/send_message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: messageToSend,
form_values: Object.keys(formValuesToSend).length > 0 ? formValuesToSend : undefined,
language: this.currentLanguage,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Send message response:', data);
if (data.task_id) {
// Add a placeholder AI message that will be updated by the progress tracker
const placeholderMessage = {
id: this.messageIdCounter++,
// content: 'Bezig met verwerken...',
sender: 'ai',
type: 'text',
timestamp: new Date().toISOString(),
taskId: data.task_id,
status: 'processing',
isTemporarilyAtBottom: true
};
this.allMessages.push(placeholderMessage);
}
} catch (error) {
console.error('Error sending message:', error);
this.addMessage({
content: 'Er is een fout opgetreden bij het verzenden van het bericht.',
sender: 'ai',
type: 'error'
});
} finally {
this.isTyping = false;
this.isLoading = false;
}
},
updateCurrentMessage(message) {
this.currentMessage = message;
},
submitFormFromInput(formValues) {
console.log('Form submitted from input:', formValues);
this.formValues = formValues;
this.sendMessage();
},
handleFileUpload(file) {
console.log('File upload:', file);
// Implement file upload logic
},
handleVoiceRecord(audioData) {
console.log('Voice record:', audioData);
// Implement voice recording logic
},
// Event handling
setupEventListeners() {
// Language change listener
document.addEventListener('language-changed', (event) => {
if (event.detail && event.detail.language) {
this.currentLanguage = event.detail.language;
console.log(`Language changed to: ${this.currentLanguage}`);
}
});
// Window resize listener
window.addEventListener('resize', () => {
this.isMobile = window.innerWidth <= 768;
this.showSidebar = window.innerWidth > 768;
});
},
cleanup() {
// Remove event listeners
document.removeEventListener('language-changed', this.handleLanguageChange);
window.removeEventListener('resize', this.handleResize);
},
// Specialist event handlers
handleSpecialistComplete(eventData) {
console.log('Specialist complete event received:', eventData);
// Find the message with the matching task ID
const messageIndex = this.allMessages.findIndex(msg =>
msg.taskId === eventData.taskId
);
if (messageIndex !== -1) {
// Update the message content
this.allMessages[messageIndex].content = eventData.answer;
this.allMessages[messageIndex].status = 'completed';
// Handle form request if present
if (eventData.form_request) {
console.log('Form request received:', eventData.form_request);
this.currentInputFormData = eventData.form_request;
}
}
this.isTyping = false;
this.isLoading = false;
},
handleSpecialistError(eventData) {
console.log('Specialist error event received:', eventData);
// Find the message with the matching task ID
const messageIndex = this.allMessages.findIndex(msg =>
msg.taskId === eventData.taskId
);
if (messageIndex !== -1) {
// Update the message to show error
this.allMessages[messageIndex].content = eventData.message || 'Er is een fout opgetreden.';
this.allMessages[messageIndex].type = 'error';
this.allMessages[messageIndex].status = 'error';
}
this.isTyping = false;
this.isLoading = false;
},
// UI helpers
scrollToBottom() {
if (this.$refs.messageHistory) {
this.$refs.messageHistory.scrollToBottom();
}
},
focusChatInput() {
if (this.$refs.chatInput) {
this.$refs.chatInput.focusInput();
}
},
// Search functionality
searchMessages(query) {
if (!query.trim()) {
this.isSearching = false;
this.filteredMessages = [];
return;
}
this.isSearching = true;
const searchTerm = query.toLowerCase();
this.filteredMessages = this.allMessages.filter(message =>
message.content &&
message.content.toLowerCase().includes(searchTerm)
);
},
clearSearch() {
this.isSearching = false;
this.filteredMessages = [];
this.messageSearch = '';
}
}
};
</script>
<style scoped>
.chat-app-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.chat-messages-area {
flex: 1;
overflow: hidden;
}
.chat-input-area {
flex-shrink: 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.chat-app-container {
height: 100vh;
}
}
</style>