Files
eveAI/eveai_chat_client/static/js/chat-app.js

679 lines
23 KiB
JavaScript

// Import all components
import { TypingIndicator } from '/static/assets/js/components/TypingIndicator.js';
import { FormField } from '/static/assets/js/components/FormField.js';
import { DynamicForm } from '/static/assets/js/components/DynamicForm.js';
import { ChatMessage } from '/static/assets/js/components/ChatMessage.js';
import { MessageHistory } from '/static/assets/js/components/MessageHistory.js';
import { ChatInput } from '/static/assets/js/components/ChatInput.js';
import { ProgressTracker } from '/static/assets/js/components/ProgressTracker.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 || {};
return {
// Base template data (keeping existing functionality)
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();
// 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.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`);
}
},
addWelcomeMessage() {
this.addMessage(
'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!',
'ai',
'text'
);
},
setupEventListeners() {
// Window resize listener
window.addEventListener('resize', this.handleResize);
// Keyboard shortcuts
document.addEventListener('keydown', this.handleGlobalKeydown);
},
cleanup() {
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('keydown', this.handleGlobalKeydown);
},
// Message management
addMessage(content, sender, type = 'text', formData = null) {
const message = {
id: this.messageIdCounter++,
content,
sender,
type,
formData,
timestamp: new Date().toISOString(),
status: sender === 'user' ? 'sent' : 'delivered'
};
this.allMessages.push(message);
// Initialize form values if it's a form
if (type === 'form' && formData) {
this.$set(this.formValues, message.id, {});
formData.fields.forEach(field => {
this.$set(this.formValues[message.id], field.name, field.defaultValue || '');
});
}
// Update search results if searching
if (this.isSearching) {
this.performSearch();
}
return message;
},
updateCurrentMessage(value) {
this.currentMessage = value;
},
// Message sending
async sendMessage() {
const text = this.currentMessage.trim();
if (!text || this.isLoading) return;
console.log('Sending message:', text);
// Add user message
const userMessage = this.addMessage(text, 'user', 'text');
this.currentMessage = '';
// Show typing and loading state
this.isTyping = true;
this.isLoading = true;
try {
const response = await this.callAPI('/api/send_message', {
message: text,
conversation_id: this.conversationId,
user_id: this.userId
});
// 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;
}
},
// Form handling
async submitForm(formData, messageId) {
this.isSubmittingForm = true;
console.log('Submitting form:', formData.title, this.formValues[messageId]);
try {
const response = await this.callAPI('/api/submit_form', {
formData: this.formValues[messageId],
formType: formData.title,
conversation_id: this.conversationId,
user_id: this.userId
});
if (response.success) {
this.addMessage(
`${response.message || 'Formulier succesvol verzonden!'}`,
'ai',
'text'
);
// Remove the form message
this.removeMessage(messageId);
} else {
this.addMessage(
`❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`,
'ai',
'text'
);
}
} 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'
);
} finally {
this.isSubmittingForm = false;
}
},
async submitFormFromInput(formValues) {
this.isSubmittingForm = true;
if (!this.currentInputFormData) {
console.error('No form data available');
return;
}
console.log('Submitting form from input:', this.currentInputFormData.title, formValues);
try {
const response = await this.callAPI('/api/submit_form', {
formData: formValues,
formType: this.currentInputFormData.title,
conversation_id: this.conversationId,
user_id: this.userId
});
if (response.success) {
this.addMessage(
`${response.message || 'Formulier succesvol verzonden!'}`,
'ai',
'text'
);
// Wis het huidige formulier
this.currentInputFormData = null;
} else {
this.addMessage(
`❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`,
'ai',
'text'
);
}
} 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'
);
} finally {
this.isSubmittingForm = 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, voeg deze toe als nieuw bericht
if (eventData.form_request) {
console.log('Adding form request as new message:', eventData.form_request);
// Converteer de form_request naar het verwachte formaat
const formData = this.convertFormRequest(eventData.form_request);
// Voeg het formulier toe als een nieuw AI bericht
this.addMessage('', 'ai', 'form', formData);
}
},
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);
// Converteer de fields van object naar array formaat
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
id: fieldId,
name: fieldDef.name,
type: fieldDef.type,
description: fieldDef.description,
required: fieldDef.required || false,
defaultValue: fieldDef.default || '',
allowedValues: fieldDef.allowed_values || null
}));
return {
title: formRequest.name,
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();
},
handleSpecialistComplete(eventData) {
console.log('ChatApp received specialist-complete:', eventData);
// Als er een form_request is, stuur deze naar de ChatInput component
if (eventData.form_request) {
console.log('Providing form request to ChatInput:', eventData.form_request);
// Converteer de form_request naar het verwachte formaat
const formData = this.convertFormRequest(eventData.form_request);
// Update de currentInputFormData voor ChatInput
this.currentInputFormData = formData;
}
},
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);
// Converteer de fields van object naar array formaat
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
id: fieldId,
name: fieldDef.name,
type: fieldDef.type,
description: fieldDef.description,
required: fieldDef.required || false,
defaultValue: fieldDef.default || '',
allowedValues: fieldDef.allowed_values || null
}));
return {
title: formRequest.name,
icon: formRequest.icon || 'form',
version: formRequest.version || '1.0',
fields: fieldsArray
};
},
},
template: `
<div class="chat-app-container">
<!-- Message History - takes available space -->
<message-history
:messages="displayMessages"
:is-typing="isTyping"
:form-values="formValues"
:is-submitting-form="isSubmittingForm"
:api-prefix="apiPrefix"
:auto-scroll="true"
@submit-form="submitForm"
@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"
@send-message="sendMessage"
@update-message="updateCurrentMessage"
@upload-file="handleFileUpload"
@record-voice="handleVoiceRecord"
@submit-form="submitFormFromInput"
ref="chatInput"
class="chat-input-area"
></chat-input>
</div>
`
};
// Zorg ervoor dat alle componenten correct geïnitialiseerd zijn voordat ze worden gebruikt
const initializeApp = () => {
console.log('Initializing Chat Application');
// Get access to the existing Vue app instance
if (window.__vueApp) {
// Zorg ervoor dat alle componenten globaal beschikbaar zijn via window
window.TypingIndicator = TypingIndicator;
window.FormField = FormField;
window.DynamicForm = DynamicForm;
window.ChatMessage = ChatMessage;
window.MessageHistory = MessageHistory;
window.ChatInput = ChatInput;
window.ProgressTracker = ProgressTracker;
// 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);
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');
}
};
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', initializeApp);