- Gewijzigde logica voor hoogtebepaling chat-input en message history, zodat ook de mobiele client correct functioneert.
591 lines
20 KiB
Vue
591 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;
|
|
flex: 1;
|
|
/* height: 100%; avoided to let flex sizing control height */
|
|
width: 100%;
|
|
min-height: 0;
|
|
max-width: 1000px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chat-messages-area {
|
|
flex: 1;
|
|
min-height: 0; /* ensure child can scroll */
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin-bottom: 20px; /* Ruimte tussen messages en input */
|
|
border-radius: 15px;
|
|
background: var(--history-background);
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
width: 100%;
|
|
max-width: 1000px; /* Optimale breedte */
|
|
margin-left: auto;
|
|
margin-right: auto; /* Horizontaal centreren */
|
|
align-self: center; /* Extra centrering in flexbox context */
|
|
}
|
|
|
|
.chat-input-area {
|
|
flex-shrink: 0;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
</style> |