- Build of the Chat Client using Vue.js
- Accompanying css - Views to serve the Chat Client - first test version of the TRACIE_SELECTION_SPECIALIST - ESS Implemented.
This commit is contained in:
523
eveai_chat_client/static/js/chat-app.js
Normal file
523
eveai_chat_client/static/js/chat-app.js
Normal file
@@ -0,0 +1,523 @@
|
||||
// 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: {},
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
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();
|
||||
},
|
||||
|
||||
handleSpecialistError(errorData) {
|
||||
console.error('Specialist error:', errorData);
|
||||
// Als we willen kunnen we hier nog extra logica toevoegen, zoals statistieken bijhouden of centraal loggen
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
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"
|
||||
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"
|
||||
@send-message="sendMessage"
|
||||
@update-message="updateCurrentMessage"
|
||||
@upload-file="handleFileUpload"
|
||||
@record-voice="handleVoiceRecord"
|
||||
ref="chatInput"
|
||||
class="chat-input-area"
|
||||
></chat-input>
|
||||
|
||||
</div>
|
||||
`
|
||||
|
||||
};
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Initializing Chat Application');
|
||||
|
||||
// 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);
|
||||
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');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user