// Import all components import { TypingIndicator } from './components/TypingIndicator.js'; import { FormField } from './components/FormField.js'; import { DynamicForm } from './components/DynamicForm.js'; import { ChatMessage } from './components/ChatMessage.js'; import { MessageHistory } from './components/MessageHistory.js'; import { ProgressTracker } from './components/ProgressTracker.js'; import { LanguageSelector } from './components/LanguageSelector.js'; // Maak componenten globaal beschikbaar voordat andere componenten worden geladen window.DynamicForm = DynamicForm; window.FormField = FormField; window.TypingIndicator = TypingIndicator; window.ChatMessage = ChatMessage; window.MessageHistory = MessageHistory; window.ProgressTracker = ProgressTracker; // Nu kunnen we ChatInput importeren nadat de benodigde componenten globaal beschikbaar zijn import { ChatInput } from './components/ChatInput.js'; // Main Chat Application export const ChatApp = { name: 'ChatApp', components: { TypingIndicator, FormField, DynamicForm, ChatMessage, MessageHistory, ChatInput }, 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 || 'nl'; const originalExplanation = chatConfig.explanation || ''; return { // Taal gerelateerde data currentLanguage: '', supportedLanguageDetails: chatConfig.supportedLanguageDetails || {}, allowedLanguages: chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'], 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); } else { console.error('Marked library not properly loaded'); return this.explanation; } }, displayMessages() { return this.isSearching ? this.filteredMessages : this.allMessages; }, hasMessages() { return this.allMessages.length > 0; } }, 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; try { // Verzamel gegevens voor de API call const apiData = { message: 'Initialize', conversation_id: this.conversationId, user_id: this.userId, language: this.currentLanguage }; const response = await this.callAPI('/api/send_message', apiData); // Verberg typing indicator this.isTyping = false; // Voeg AI response toe met task_id voor tracking const aiMessage = this.addMessage( '', 'ai', 'text' ); // Voeg task_id toe als beschikbaar if (response.task_id) { console.log('Monitoring Initialize Task ID: ', response.task_id); aiMessage.taskId = response.task_id; } } catch (error) { console.error('Error sending initialize message:', error); this.isTyping = false; // Voeg standaard welkomstbericht toe als fallback this.addMessage( 'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!', 'ai', 'text' ); } finally { this.isLoading = false; } }, setupEventListeners() { // Window resize listener window.addEventListener('resize', this.handleResize); // Keyboard shortcuts document.addEventListener('keydown', this.handleGlobalKeydown); // Luister naar taalwijzigingen via custom events document.addEventListener('language-changed', (event) => { if (event.detail && event.detail.language) { console.log('ChatApp received language-changed event:', event.detail.language); this.handleLanguageChange(event.detail.language); } }); }, cleanup() { window.removeEventListener('resize', this.handleResize); document.removeEventListener('keydown', this.handleGlobalKeydown); }, // Taal gerelateerde functionaliteit handleLanguageChange(newLanguage) { if (this.currentLanguage !== newLanguage) { console.log(`ChatApp: Taal gewijzigd van ${this.currentLanguage} naar ${newLanguage}`); this.currentLanguage = newLanguage; // Vertaal de sidebar this.translateSidebar(newLanguage); // Sla de taalvoorkeur op voor toekomstige API calls this.storeLanguagePreference(newLanguage); // Stuur language-changed event voor andere componenten (zoals ChatInput) // Dit wordt gedaan via het event systeem, waardoor we geen directe referentie nodig hebben const event = new CustomEvent('language-changed', { detail: { language: newLanguage } }); document.dispatchEvent(event); } }, // Maak de handleLanguageChange methode toegankelijk van buitenaf // Deze functie wordt opgeroepen door het externe LanguageSelector component __handleExternalLanguageChange(newLanguage) { this.handleLanguageChange(newLanguage); }, storeLanguagePreference(language) { // Sla op in localStorage voor persistentie localStorage.setItem('preferredLanguage', language); // Update chatConfig voor toekomstige API calls if (window.chatConfig) { window.chatConfig.language = language; } console.log(`Taalvoorkeur opgeslagen: ${language}`); }, async translateSidebar(language) { console.log(`Sidebar wordt vertaald naar: ${language}`); // Haal de originele tekst op const originalText = this.originalExplanation || this.explanation; try { // Controleer of TranslationClient beschikbaar is if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') { console.error('TranslationClient.translate is niet beschikbaar'); this.showTranslationIndicator(language, 'Vertaling niet beschikbaar', false); return; } // Toon loading indicator this.showTranslationIndicator(language, 'Bezig met vertalen...'); console.log('API prefix voor vertaling:', this.apiPrefix); // Gebruik TranslationClient met de juiste parameters const response = await window.TranslationClient.translate( originalText, language, null, // source_lang (auto-detect) 'sidebar_explanation', // context this.apiPrefix // API prefix voor tenant routing ); if (response.success) { // Update de explanation variabele console.log('Translated text: ' + response.translated_text); this.explanation = response.translated_text; // 1. Update de Vue instance if (window.__vueApp && window.__vueApp._instance) { window.__vueApp._instance.proxy.explanation = response.translated_text; } // 2. Update direct het DOM-element via marked voor onmiddellijke weergave const sidebarElement = document.querySelector('.sidebar-explanation'); if (sidebarElement) { console.log('DOM-element gevonden, directe update toepassen'); // Gebruik de marked library om de markdown naar HTML te converteren let htmlContent; if (typeof marked === 'function') { htmlContent = marked(response.translated_text); } else if (marked && typeof marked.parse === 'function') { htmlContent = marked.parse(response.translated_text); } else { htmlContent = response.translated_text; } // Update de inhoud direct sidebarElement.innerHTML = htmlContent; } else { console.error('Sidebar explanation element niet gevonden in DOM'); } this.showTranslationIndicator(language, 'Vertaling voltooid!', true); } else { console.error('Vertaling mislukt:', response.error); this.showTranslationIndicator(language, 'Vertaling mislukt', false); } } catch (error) { console.error('Fout bij vertalen sidebar:', error); this.showTranslationIndicator(language, 'Vertaling mislukt', false); } }, // Message management addMessage(content, sender, type = 'text', formData = null, formValues = null) { const message = { id: this.messageIdCounter++, content, sender, type, formData, formValues, timestamp: new Date().toISOString(), status: sender === 'user' ? 'sent' : 'delivered' }; this.allMessages.push(message); // Initialize form values if it's a form and no values were provided if (type === 'form' && formData && !formValues) { // Vue 3 compatibele manier om reactieve objecten bij te werken this.formValues[message.id] = {}; formData.fields.forEach(field => { const fieldName = field.name || field.id; if (fieldName) { this.formValues[message.id][fieldName] = field.defaultValue || ''; } }); } // Update search results if searching if (this.isSearching) { this.performSearch(); } return message; }, showTranslationIndicator(language, message, success = null) { const explanationElement = document.querySelector('.sidebar-explanation'); if (explanationElement) { // Verwijder eventuele bestaande indicators const existingIndicator = explanationElement.querySelector('.language-change-indicator'); if (existingIndicator) { existingIndicator.remove(); } // Voeg nieuwe indicator toe const indicator = document.createElement('div'); indicator.className = 'language-change-indicator'; if (success === true) indicator.classList.add('success'); if (success === false) indicator.classList.add('error'); indicator.innerHTML = `${message}`; explanationElement.prepend(indicator); // Verwijder na 3 seconden, behalve bij loading if (success !== null) { setTimeout(() => { if (explanationElement.contains(indicator)) { indicator.remove(); } }, 3000); } } }, // Helper functie om formulierdata toe te voegen aan bestaande berichten attachFormDataToMessage(messageId, formData, formValues) { const message = this.allMessages.find(m => m.id === messageId); if (message) { message.formData = formData; message.formValues = formValues; } }, updateCurrentMessage(value) { this.currentMessage = value; }, // Message sending (alleen voor gewone tekstberichten, geen formulieren) async sendMessage() { const text = this.currentMessage.trim(); // Controleer of we kunnen verzenden if (!text || this.isLoading) return; console.log('Sending text message:', text); // Add user message const userMessage = this.addMessage(text, 'user', 'text'); // Wis input this.currentMessage = ''; // Show typing and loading state this.isTyping = true; this.isLoading = true; try { // Verzamel gegevens voor de API call const apiData = { message: text, conversation_id: this.conversationId, user_id: this.userId, language: this.currentLanguage }; const response = await this.callAPI('/api/send_message', apiData); // Hide typing indicator this.isTyping = false; // Mark user message as delivered userMessage.status = 'delivered'; // Add AI response if (response.type === 'form') { this.addMessage('', 'ai', 'form', response.formData); } else { // Voeg het bericht toe met task_id voor tracking - initieel leeg const aiMessage = this.addMessage( '', 'ai', 'text' ); // Voeg task_id toe als beschikbaar if (response.task_id) { console.log('Monitoring Task ID: ', response.task_id); aiMessage.taskId = response.task_id; } } } catch (error) { console.error('Error sending message:', error); this.isTyping = false; // Mark user message as failed userMessage.status = 'failed'; this.addMessage( 'Sorry, er ging iets mis bij het verzenden van je bericht. Probeer het opnieuw.', 'ai', 'error' ); } finally { this.isLoading = false; } }, async submitFormFromInput(formValues) { this.isSubmittingForm = true; if (!this.currentInputFormData) { console.error('No form data available'); return; } console.log('Form values received:', formValues); console.log('Current input form data:', this.currentInputFormData); try { // Maak een user message met formuliergegevens én eventuele tekst const userMessage = this.addMessage( this.currentMessage.trim(), // Voeg tekst toe als die er is 'user', 'text' ); // Voeg formuliergegevens toe aan het bericht userMessage.formData = this.currentInputFormData; userMessage.formValues = formValues; // Reset het tekstbericht this.currentMessage = ''; this.$emit('update-message', ''); // Toon laad-indicator this.isTyping = true; this.isLoading = true; // Verzamel gegevens voor de API call const apiData = { message: userMessage.content, conversation_id: this.conversationId, user_id: this.userId, form_values: formValues // Voeg formuliergegevens toe aan API call }; // Verstuur bericht naar de API const response = await this.callAPI('/api/send_message', apiData); // Verberg de typing indicator this.isTyping = false; // Markeer het gebruikersbericht als afgeleverd userMessage.status = 'delivered'; // Voeg AI response toe met task_id voor tracking const aiMessage = this.addMessage( '', 'ai', 'text' ); if (response.task_id) { console.log('Monitoring Task ID: ', response.task_id); aiMessage.taskId = response.task_id; } // Reset formulier na succesvolle verzending this.currentInputFormData = null; } catch (error) { console.error('Error submitting form:', error); this.addMessage( 'Sorry, er ging iets mis bij het verzenden van het formulier. Probeer het opnieuw.', 'ai', 'text' ); // Wis ook hier het formulier na een fout this.currentInputFormData = null; } finally { this.isSubmittingForm = false; this.isLoading = false; } }, // Message actions retryMessage(messageId) { const message = this.allMessages.find(m => m.id === messageId); if (message && message.status === 'failed') { // Retry sending the message this.currentMessage = message.content; this.removeMessage(messageId); this.sendMessage(); } }, removeMessage(messageId) { const index = this.allMessages.findIndex(m => m.id === messageId); if (index !== -1) { this.allMessages.splice(index, 1); // Verwijder ook eventuele formuliergegevens if (this.formValues[messageId]) { delete this.formValues[messageId]; } } }, // File handling async handleFileUpload(file) { console.log('Uploading file:', file.name); // Add file message const fileMessage = this.addMessage('', 'user', 'file', { fileName: file.name, fileSize: this.formatFileSize(file.size), fileType: file.type }); try { // TODO: Implement actual file upload // const response = await this.uploadFile(file); // fileMessage.fileUrl = response.url; // Simulate file upload setTimeout(() => { fileMessage.fileUrl = URL.createObjectURL(file); fileMessage.status = 'delivered'; }, 1000); } catch (error) { console.error('Error uploading file:', error); fileMessage.status = 'failed'; } }, async handleVoiceRecord(audioBlob) { console.log('Processing voice recording'); // Add voice message const voiceMessage = this.addMessage('', 'user', 'voice', { audioBlob, duration: '00:05' // TODO: Calculate actual duration }); // TODO: Send to speech-to-text service // const transcription = await this.transcribeAudio(audioBlob); // this.currentMessage = transcription; // this.sendMessage(); }, // Search functionality performSearch() { if (!this.messageSearch.trim()) { this.isSearching = false; this.filteredMessages = []; return; } this.isSearching = true; const query = this.messageSearch.toLowerCase(); this.filteredMessages = this.allMessages.filter(message => message.content && message.content.toLowerCase().includes(query) ); }, clearSearch() { this.messageSearch = ''; this.isSearching = false; this.filteredMessages = []; }, // Event handlers voor specialist events handleSpecialistComplete(eventData) { console.log('ChatApp received specialist-complete:', eventData); // Als er een form_request is, toon deze in de ChatInput component if (eventData.form_request) { console.log('Setting form request in ChatInput:', eventData.form_request); try { // Converteer de form_request naar het verwachte formaat const formData = this.convertFormRequest(eventData.form_request); // Stel het formulier in als currentInputFormData in plaats van als bericht toe te voegen if (formData && formData.title && formData.fields) { this.currentInputFormData = formData; } else { console.error('Invalid form data after conversion:', formData); } } catch (err) { console.error('Error processing form request:', err); } } }, handleSpecialistError(eventData) { console.log('ChatApp received specialist-error:', eventData); // Voeg foutbericht toe this.addMessage( eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.', 'ai', 'error' ); }, // Helper methode om form_request te converteren naar het verwachte formaat convertFormRequest(formRequest) { console.log('Converting form request:', formRequest); if (!formRequest) { console.error('Geen geldig formRequest ontvangen'); return null; } // Controleer of fields een object is voordat we converteren let fieldsArray; if (formRequest.fields && typeof formRequest.fields === 'object' && !Array.isArray(formRequest.fields)) { // Converteer de fields van object naar array formaat fieldsArray = Object.entries(formRequest.fields).map(([fieldId, fieldDef]) => ({ id: fieldId, name: fieldDef.name || fieldId, // Gebruik fieldId als fallback type: fieldDef.type || 'text', // Standaard naar text description: fieldDef.description || '', required: fieldDef.required || false, default: fieldDef.default || '', allowedValues: fieldDef.allowed_values || null, context: fieldDef.context || null })); } else if (Array.isArray(formRequest.fields)) { // Als het al een array is, zorg dat alle velden correct zijn fieldsArray = formRequest.fields.map(field => ({ id: field.id || field.name, name: field.name || field.id, type: field.type || 'text', description: field.description || '', required: field.required || false, default: field.default || field.defaultValue || '', allowedValues: field.allowed_values || field.allowedValues || null, context: field.context || null })); } else { // Fallback naar lege array als er geen velden zijn console.warn('Formulier heeft geen geldige velden'); fieldsArray = []; } return { title: formRequest.name || formRequest.title || 'Formulier', description: formRequest.description || '', icon: formRequest.icon || 'form', version: formRequest.version || '1.0', fields: fieldsArray }; }, // Event handlers handleResize() { this.isMobile = window.innerWidth <= 768; this.showSidebar = window.innerWidth > 768; }, handleGlobalKeydown(event) { // Ctrl/Cmd + K for search if ((event.ctrlKey || event.metaKey) && event.key === 'k') { event.preventDefault(); this.focusSearch(); } // Escape to clear search if (event.key === 'Escape' && this.isSearching) { this.clearSearch(); } }, // Utility methods async callAPI(endpoint, data) { // Gebruik de API prefix uit de lokale variabele const fullEndpoint = this.apiPrefix + '/chat' + endpoint; console.log('Calling API with prefix:', { prefix: this.apiPrefix, endpoint: endpoint, fullEndpoint: fullEndpoint }); const response = await fetch(fullEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, focusChatInput() { this.$refs.chatInput?.focusInput(); }, focusSearch() { this.$refs.searchInput?.focus(); }, }, template: `
` }; // Zorg ervoor dat alle componenten correct geïnitialiseerd zijn voordat ze worden gebruikt const initializeApp = () => { console.log('Initializing Chat Application'); // ChatInput wordt pas op dit punt globaal beschikbaar gemaakt // omdat het afhankelijk is van andere componenten window.ChatInput = ChatInput; // Get access to the existing Vue app instance if (window.__vueApp) { // Register ALL components globally window.__vueApp.component('TypingIndicator', TypingIndicator); window.__vueApp.component('FormField', FormField); window.__vueApp.component('DynamicForm', DynamicForm); window.__vueApp.component('ChatMessage', ChatMessage); window.__vueApp.component('MessageHistory', MessageHistory); window.__vueApp.component('ChatInput', ChatInput); window.__vueApp.component('ProgressTracker', ProgressTracker); // NB: LanguageSelector wordt niet globaal geregistreerd omdat deze apart gemonteerd wordt console.log('All chat components registered with existing Vue instance'); // Register the ChatApp component window.__vueApp.component('ChatApp', ChatApp); console.log('ChatApp component registered with existing Vue instance'); // Mount the Vue app window.__vueApp.mount('#app'); console.log('Vue app mounted with chat components'); } else { console.error('No existing Vue instance found on window.__vueApp'); } }; // Functie om LanguageSelector toe te voegen aan sidebar const mountLanguageSelector = () => { const container = document.getElementById('language-selector-container'); if (container) { // Maak een eenvoudige Vue app die alleen de LanguageSelector component mount const app = Vue.createApp({ components: { LanguageSelector }, data() { return { currentLanguage: window.chatConfig?.language || 'nl', supportedLanguageDetails: window.chatConfig?.supportedLanguageDetails || {}, allowedLanguages: window.chatConfig?.allowedLanguages || ['nl', 'en', 'fr', 'de'] }; }, methods: { handleLanguageChange(newLanguage) { console.log(`LanguageSelector: Taal gewijzigd naar ${newLanguage}`); // Gebruik ALLEEN de custom event benadering const event = new CustomEvent('language-changed', { detail: { language: newLanguage } }); document.dispatchEvent(event); } }, template: ` ` }); app.component('LanguageSelector', LanguageSelector); app.mount('#language-selector-container'); console.log('Language selector mounted in sidebar'); } else { console.warn('Language selector container not found'); } }; // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { console.log('DOM content loaded, initializing application...'); // Eerst de hoofdapplicatie initialiseren initializeApp(); // Dan de taal selector monteren met een kleine vertraging // om er zeker van te zijn dat de container is aangemaakt setTimeout(() => { try { mountLanguageSelector(); } catch (e) { console.error('Fout bij het monteren van de taal selector:', e); } }, 200); });