- Introduction of Shells for Mobile client and Desktop client. Extensible with additional shells in the future
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dist/chat-client.js": "dist/chat-client.5b709f8c.js",
|
||||
"dist/chat-client.css": "dist/chat-client.cb306abb.css",
|
||||
"dist/chat-client.js": "dist/chat-client.be407684.js",
|
||||
"dist/chat-client.css": "dist/chat-client.00145e73.css",
|
||||
"dist/main.js": "dist/main.6a617099.js",
|
||||
"dist/main.css": "dist/main.7182aac3.css"
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
:is-latest-ai-message="true"
|
||||
:is-in-input-area="true"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
@specialist-complete="handleSpecialistCompleteFromActiveMessage"
|
||||
@specialist-error="handleSpecialistErrorFromActiveMessage"
|
||||
></chat-message>
|
||||
</div>
|
||||
|
||||
@@ -183,22 +183,22 @@ export default {
|
||||
watch: {
|
||||
formData: {
|
||||
handler(newFormData, oldFormData) {
|
||||
console.log('ChatInput formData changed:', newFormData);
|
||||
console.log('🧐 [ChatInput] formData changed:', newFormData);
|
||||
|
||||
if (!newFormData) {
|
||||
console.log('FormData is null of undefined');
|
||||
console.log('🧐 [ChatInput] formData is null of undefined');
|
||||
this.formValues = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// Controleer of velden aanwezig zijn
|
||||
if (!newFormData.fields) {
|
||||
console.error('FormData bevat geen velden!', newFormData);
|
||||
console.error('🧐 [ChatInput] formData bevat geen velden!', newFormData);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Velden in formData:', newFormData.fields);
|
||||
console.log('Aantal velden:', Array.isArray(newFormData.fields)
|
||||
console.log('🧐 [ChatInput] velden in formData:', newFormData.fields);
|
||||
console.log('🧐 [ChatInput] aantal velden:', Array.isArray(newFormData.fields)
|
||||
? newFormData.fields.length
|
||||
: Object.keys(newFormData.fields).length);
|
||||
|
||||
@@ -206,7 +206,7 @@ export default {
|
||||
this.initFormValues();
|
||||
|
||||
// Log de geïnitialiseerde waarden
|
||||
console.log('Formulierwaarden geïnitialiseerd:', this.formValues);
|
||||
console.log('🧐 [ChatInput] formulierwaarden geïnitialiseerd:', this.formValues);
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
@@ -251,6 +251,15 @@ export default {
|
||||
window.removeEventListener('resize', this.autoResize);
|
||||
},
|
||||
methods: {
|
||||
handleSpecialistCompleteFromActiveMessage(eventData) {
|
||||
console.log('🧐 [ChatInput] specialist-complete ontvangen van actieve ChatMessage, bubbelt naar parent:', eventData);
|
||||
this.$emit('specialist-complete', eventData);
|
||||
},
|
||||
|
||||
handleSpecialistErrorFromActiveMessage(eventData) {
|
||||
console.log('🧐 [ChatInput] specialist-error ontvangen van actieve ChatMessage, bubbelt naar parent:', eventData);
|
||||
this.$emit('specialist-error', eventData);
|
||||
},
|
||||
handleLanguageChange(event) {
|
||||
if (event.detail && event.detail.language) {
|
||||
this.translatePlaceholder(event.detail.language);
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
<template>
|
||||
<!--
|
||||
ChatRoot
|
||||
--------
|
||||
Root-component voor de chatclient. Deze component zorgt ervoor dat er
|
||||
altijd precies één SafeViewport-wrapper is rond de gekozen Shell
|
||||
(DesktopChatShell, MobileChatShell, ...).
|
||||
|
||||
De daadwerkelijke shell-component en zijn props worden vanuit
|
||||
chat-client.js doorgegeven via de props shellComponent en shellProps.
|
||||
-->
|
||||
<SafeViewport>
|
||||
<ChatApp />
|
||||
<component :is="shellComponent" v-bind="shellProps" />
|
||||
</SafeViewport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// ChatRoot.vue
|
||||
// Kleine root-component die de ChatApp binnen de SafeViewport wrapper rendert.
|
||||
|
||||
import ChatApp from './ChatApp.vue';
|
||||
import { computed } from 'vue';
|
||||
import SafeViewport from './SafeViewport.vue';
|
||||
import DesktopChatShell from './DesktopChatShell.vue';
|
||||
import MobileChatShell from './MobileChatShell.vue';
|
||||
|
||||
const props = defineProps({
|
||||
shellComponent: {
|
||||
type: [Object, Function, String],
|
||||
default: null
|
||||
},
|
||||
shellProps: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// Fallbacks voor het geval chat-client.js geen shellComponent meegeeft.
|
||||
const resolvedShellComponent = computed(() => {
|
||||
if (props.shellComponent) {
|
||||
return props.shellComponent;
|
||||
}
|
||||
|
||||
// Eenvoudige fallback: gebruik DesktopShell op brede schermen,
|
||||
// MobileShell op smalle schermen.
|
||||
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
|
||||
return MobileChatShell;
|
||||
}
|
||||
return DesktopChatShell;
|
||||
});
|
||||
|
||||
const shellComponent = resolvedShellComponent;
|
||||
const shellProps = props.shellProps;
|
||||
</script>
|
||||
|
||||
501
eveai_chat_client/static/assets/vue-components/CoreChatApp.vue
Normal file
501
eveai_chat_client/static/assets/vue-components/CoreChatApp.vue
Normal file
@@ -0,0 +1,501 @@
|
||||
<template>
|
||||
<!--
|
||||
CoreChatApp
|
||||
------------
|
||||
Deze component bevat alle kernlogica en state van de chat, maar legt zelf
|
||||
geen definitieve layout op. In plaats daarvan levert hij named slots aan
|
||||
waar Shells (Desktop/Mobile/Plugin) hun eigen opbouw kunnen definieren.
|
||||
|
||||
Beschikbare slots:
|
||||
- history: weergave van de berichtenhistoriek
|
||||
- active-message-input: actieve AI-boodschap + invoergebied
|
||||
- setup: configuratiepaneel (uitleg, taal, ...)
|
||||
-->
|
||||
<div
|
||||
class="core-chat-app"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
@specialist-error="handleSpecialistError"
|
||||
>
|
||||
<!-- History-paneel (optioneel) -->
|
||||
<slot
|
||||
name="history"
|
||||
:messages="displayMessages"
|
||||
:is-typing="isTyping"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:on-specialist-complete="handleSpecialistComplete"
|
||||
:on-specialist-error="handleSpecialistError"
|
||||
/>
|
||||
|
||||
<!-- Actieve boodschap + invoer (optioneel) -->
|
||||
<slot
|
||||
name="active-message-input"
|
||||
:active-ai-message="activeAiMessage"
|
||||
:form-data="currentInputFormData"
|
||||
:current-message="currentMessage"
|
||||
:is-loading="isLoading"
|
||||
:on-send-message="sendMessage"
|
||||
:on-update-message="updateCurrentMessage"
|
||||
:on-submit-form="submitFormFromInput"
|
||||
:on-upload-file="handleFileUpload"
|
||||
:on-record-voice="handleVoiceRecord"
|
||||
:on-specialist-complete="handleSpecialistComplete"
|
||||
:on-specialist-error="handleSpecialistError"
|
||||
:form-values="formValues"
|
||||
/>
|
||||
|
||||
<!-- Setup-paneel (optioneel) -->
|
||||
<slot
|
||||
name="setup"
|
||||
:tenant-name="tenantName"
|
||||
:tenant-subtitle="tenantSubtitle"
|
||||
:explanation-text="originalExplanation"
|
||||
:current-language="currentLanguage"
|
||||
:supported-language-details="supportedLanguageDetails"
|
||||
:allowed-languages="allowedLanguages"
|
||||
:api-prefix="apiPrefix"
|
||||
:on-language-changed="handleLanguageChangedFromSetup"
|
||||
/>
|
||||
|
||||
<!-- Content modal op Core-niveau zodat alle shells deze kunnen gebruiken -->
|
||||
<ContentModal
|
||||
: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 MessageHistory from './MessageHistory.vue';
|
||||
import ChatInput from './ChatInput.vue';
|
||||
import ContentModal from './ContentModal.vue';
|
||||
import TypingIndicator from './TypingIndicator.vue';
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
import ChatMessage from './ChatMessage.vue';
|
||||
import ProgressTracker from './ProgressTracker.vue';
|
||||
|
||||
import { provide } from 'vue';
|
||||
import { provideContentModal } from '../js/composables/useContentModal.js';
|
||||
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
|
||||
|
||||
export default {
|
||||
name: 'CoreChatApp',
|
||||
|
||||
components: {
|
||||
MessageHistory,
|
||||
ChatInput,
|
||||
ContentModal,
|
||||
TypingIndicator,
|
||||
DynamicForm,
|
||||
ChatMessage,
|
||||
ProgressTracker
|
||||
},
|
||||
|
||||
props: {
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
userId: {
|
||||
type: [String, Number, null],
|
||||
default: null
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialLanguage: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
supportedLanguageDetails: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowedLanguages: {
|
||||
type: Array,
|
||||
default: () => ['nl', 'en', 'fr', 'de']
|
||||
},
|
||||
// De volledige chatConfig kan optioneel meegegeven worden voor
|
||||
// backwards compatibility; CoreChatApp leest daaruit wat hij nodig heeft.
|
||||
chatConfig: {
|
||||
type: Object,
|
||||
default: () => (window.chatConfig || {})
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const contentModal = provideContentModal();
|
||||
|
||||
// LanguageProvider opnieuw centraliseren rond CoreChatApp zodat alle
|
||||
// child-componenten (ChatInput, SideBarExplanation, ...) veilig
|
||||
// useLanguageProvider kunnen gebruiken, ongeacht de gebruikte Shell.
|
||||
const initialLanguage = props.initialLanguage || 'nl';
|
||||
const apiPrefix = props.apiPrefix || '';
|
||||
const languageProvider = createLanguageProvider(initialLanguage, apiPrefix);
|
||||
provide(LANGUAGE_PROVIDER_KEY, languageProvider);
|
||||
|
||||
return {
|
||||
contentModal
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
const chatConfig = this.chatConfig || window.chatConfig || {};
|
||||
const settings = chatConfig.settings || {};
|
||||
const initialLanguage = chatConfig.language || this.initialLanguage || 'en';
|
||||
const originalExplanation = chatConfig.explanation || '';
|
||||
const tenantMake = chatConfig.tenantMake || {};
|
||||
|
||||
return {
|
||||
// Tenant info
|
||||
tenantName: tenantMake.name || 'EveAI',
|
||||
tenantSubtitle: tenantMake.subtitle || '',
|
||||
|
||||
// Taal gerelateerde data
|
||||
currentLanguage: initialLanguage,
|
||||
supportedLanguageDetailsInternal: chatConfig.supportedLanguageDetails || this.supportedLanguageDetails,
|
||||
allowedLanguagesInternal: chatConfig.allowedLanguages || this.allowedLanguages,
|
||||
originalExplanation,
|
||||
|
||||
// Chat-specific data
|
||||
currentMessage: '',
|
||||
allMessages: [],
|
||||
isTyping: false,
|
||||
isLoading: false,
|
||||
isSubmittingForm: false,
|
||||
messageIdCounter: 1,
|
||||
formValues: {},
|
||||
currentInputFormData: null,
|
||||
|
||||
// Configuration from server
|
||||
settings: {
|
||||
maxMessageLength: settings.maxMessageLength || 2000,
|
||||
allowFileUpload: settings.allowFileUpload === true,
|
||||
allowVoiceMessage: settings.allowVoiceMessage === true,
|
||||
autoScroll: settings.autoScroll === true
|
||||
},
|
||||
|
||||
// Search state (voor history-tab)
|
||||
messageSearch: '',
|
||||
filteredMessages: [],
|
||||
isSearching: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayMessages() {
|
||||
return this.isSearching ? this.filteredMessages : this.allMessages;
|
||||
},
|
||||
|
||||
activeAiMessage() {
|
||||
return this.allMessages.find(msg => msg.isTemporarilyAtBottom);
|
||||
},
|
||||
|
||||
hasMessages() {
|
||||
return this.allMessages.length > 0;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initializeChat();
|
||||
this.setupEventListeners();
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.cleanup();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Initialisatie
|
||||
initializeChat() {
|
||||
this.loadHistoricalMessages();
|
||||
|
||||
if (this.allMessages.length === 0) {
|
||||
this.addWelcomeMessage();
|
||||
}
|
||||
},
|
||||
|
||||
loadHistoricalMessages() {
|
||||
const chatConfig = this.chatConfig || window.chatConfig || {};
|
||||
const historicalMessages = chatConfig.messages || [];
|
||||
|
||||
if (historicalMessages.length > 0) {
|
||||
this.allMessages = historicalMessages
|
||||
.filter(msg => msg !== null && msg !== undefined)
|
||||
.map(msg => ({
|
||||
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'
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
async addWelcomeMessage() {
|
||||
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: '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();
|
||||
|
||||
if (data.task_id) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
// Messages
|
||||
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);
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
if (!this.currentMessage.trim() && !this.currentInputFormData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Eerdere placeholder AI-berichten terugplaatsen in de flow
|
||||
this.repositionLatestAiMessage();
|
||||
|
||||
const userMessage = this.addMessage({
|
||||
content: this.currentMessage,
|
||||
sender: 'user',
|
||||
formData: this.currentInputFormData,
|
||||
formValues: this.formValues
|
||||
});
|
||||
|
||||
const messageToSend = this.currentMessage;
|
||||
const formValuesToSend = { ...this.formValues };
|
||||
this.currentMessage = '';
|
||||
this.formValues = {};
|
||||
this.currentInputFormData = null;
|
||||
|
||||
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();
|
||||
|
||||
if (data.task_id) {
|
||||
const placeholderMessage = {
|
||||
id: this.messageIdCounter++,
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
repositionLatestAiMessage() {
|
||||
const aiMessage = this.allMessages.find(msg =>
|
||||
msg.sender === 'ai' && msg.taskId && msg.isTemporarilyAtBottom
|
||||
);
|
||||
if (aiMessage) {
|
||||
aiMessage.isTemporarilyAtBottom = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentMessage(message) {
|
||||
this.currentMessage = message;
|
||||
},
|
||||
|
||||
submitFormFromInput(formValues) {
|
||||
this.formValues = formValues;
|
||||
this.sendMessage();
|
||||
},
|
||||
|
||||
handleFileUpload(file) {
|
||||
console.log('File upload:', file);
|
||||
},
|
||||
|
||||
handleVoiceRecord(audioData) {
|
||||
console.log('Voice record:', audioData);
|
||||
},
|
||||
|
||||
// Specialist events
|
||||
handleSpecialistComplete(eventData) {
|
||||
console.log('🧐 [CoreChatApp] specialist-complete ontvangen:', eventData);
|
||||
|
||||
const messageIndex = this.allMessages.findIndex(msg => {
|
||||
const matches = msg.taskId === eventData.taskId;
|
||||
console.log('🧐 [CoreChatApp] zoeken naar bericht voor specialist-complete:', {
|
||||
messageId: msg.id,
|
||||
messageTaskId: msg.taskId,
|
||||
eventTaskId: eventData.taskId,
|
||||
matches
|
||||
});
|
||||
return matches;
|
||||
});
|
||||
if (messageIndex !== -1) {
|
||||
console.log('🧐 [CoreChatApp] gevonden bericht voor specialist-complete:', this.allMessages[messageIndex]);
|
||||
|
||||
this.allMessages[messageIndex].content = eventData.answer;
|
||||
this.allMessages[messageIndex].status = 'completed';
|
||||
|
||||
if (eventData.form_request) {
|
||||
console.log('🧐 [CoreChatApp] form_request ontvangen, wordt ingesteld als currentInputFormData:', eventData.form_request);
|
||||
this.currentInputFormData = eventData.form_request;
|
||||
} else {
|
||||
console.log('🧐 [CoreChatApp] geen form_request ontvangen, formulier wordt gewist');
|
||||
this.currentInputFormData = null;
|
||||
if (this.formValues) {
|
||||
this.formValues = {};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('🧐 [CoreChatApp] geen bericht gevonden voor specialist-complete met taskId:', eventData.taskId);
|
||||
}
|
||||
|
||||
this.isTyping = false;
|
||||
this.isLoading = false;
|
||||
|
||||
console.log('🧐 [CoreChatApp] currentInputFormData na complete:', this.currentInputFormData);
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
const messageIndex = this.allMessages.findIndex(msg => msg.taskId === eventData.taskId);
|
||||
if (messageIndex !== -1) {
|
||||
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;
|
||||
},
|
||||
|
||||
// Event listeners voor taal & resize
|
||||
setupEventListeners() {
|
||||
this._languageChangeHandler = (event) => {
|
||||
if (event.detail && event.detail.language) {
|
||||
this.currentLanguage = event.detail.language;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('language-changed', this._languageChangeHandler);
|
||||
|
||||
this._resizeHandler = () => {
|
||||
// Placeholder voor toekomstige shell-specifieke gedrag; Core hoeft
|
||||
// hier in principe niets layout-gerelateerd te doen.
|
||||
};
|
||||
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
if (this._languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this._languageChangeHandler);
|
||||
}
|
||||
if (this._resizeHandler) {
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
}
|
||||
},
|
||||
|
||||
handleLanguageChangedFromSetup(newLanguage) {
|
||||
this.currentLanguage = newLanguage;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.core-chat-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<!--
|
||||
DesktopChatShell
|
||||
-----------------
|
||||
Shell voor brede schermen (desktop/tablet-landscape) die de CoreChatApp
|
||||
in een klassieke layout toont: history boven, invoer onder.
|
||||
|
||||
Let op: de linker Sidebar wordt nog steeds apart gemount op
|
||||
#sidebar-container door chat-client.js. Deze shell is uitsluitend
|
||||
verantwoordelijk voor de rechter chatkolom.
|
||||
-->
|
||||
<div class="desktop-chat-shell">
|
||||
<CoreChatApp
|
||||
:api-prefix="apiPrefix"
|
||||
:conversation-id="conversationId"
|
||||
:user-id="userId"
|
||||
:user-name="userName"
|
||||
:initial-language="initialLanguage"
|
||||
:supported-language-details="supportedLanguageDetails"
|
||||
:allowed-languages="allowedLanguages"
|
||||
>
|
||||
<!-- History-paneel -->
|
||||
<template #history="historyProps">
|
||||
<MessageHistory
|
||||
:messages="historyProps.messages"
|
||||
:is-typing="historyProps.isTyping"
|
||||
:is-submitting-form="historyProps.isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:auto-scroll="settings.autoScroll"
|
||||
@specialist-error="event => {
|
||||
console.log('🧐 [DesktopChatShell] specialist-error vanuit history ontvangen:', event);
|
||||
(historyProps.onSpecialistError || handleSpecialistError)(event);
|
||||
}"
|
||||
@specialist-complete="event => {
|
||||
console.log('🧐 [DesktopChatShell] specialist-complete vanuit history ontvangen:', event);
|
||||
(historyProps.onSpecialistComplete || handleSpecialistComplete)(event);
|
||||
}"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actieve boodschap + invoer -->
|
||||
<template #active-message-input="inputProps">
|
||||
<ChatInput
|
||||
:current-message="inputProps.currentMessage"
|
||||
:is-loading="inputProps.isLoading"
|
||||
:max-length="settings.maxMessageLength"
|
||||
:allow-file-upload="settings.allowFileUpload"
|
||||
:allow-voice-message="settings.allowVoiceMessage"
|
||||
:form-data="inputProps.formData"
|
||||
:active-ai-message="inputProps.activeAiMessage"
|
||||
:api-prefix="apiPrefix"
|
||||
@send-message="inputProps.onSendMessage"
|
||||
@update-message="inputProps.onUpdateMessage"
|
||||
@upload-file="inputProps.onUploadFile"
|
||||
@record-voice="inputProps.onRecordVoice"
|
||||
@submit-form="inputProps.onSubmitForm"
|
||||
@specialist-error="event => {
|
||||
console.log('🧐 [DesktopChatShell] specialist-error vanuit ChatInput ontvangen:', event);
|
||||
(inputProps.onSpecialistError || handleSpecialistError)(event);
|
||||
}"
|
||||
@specialist-complete="event => {
|
||||
console.log('🧐 [DesktopChatShell] specialist-complete vanuit ChatInput ontvangen:', event);
|
||||
(inputProps.onSpecialistComplete || handleSpecialistComplete)(event);
|
||||
}"
|
||||
ref="chatInput"
|
||||
class="chat-input-area"
|
||||
/>
|
||||
</template>
|
||||
</CoreChatApp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CoreChatApp from './CoreChatApp.vue';
|
||||
import MessageHistory from './MessageHistory.vue';
|
||||
import ChatInput from './ChatInput.vue';
|
||||
|
||||
export default {
|
||||
name: 'DesktopChatShell',
|
||||
components: {
|
||||
CoreChatApp,
|
||||
MessageHistory,
|
||||
ChatInput
|
||||
},
|
||||
|
||||
props: {
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
userId: {
|
||||
type: [String, Number, null],
|
||||
default: null
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialLanguage: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
supportedLanguageDetails: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowedLanguages: {
|
||||
type: Array,
|
||||
default: () => ['nl', 'en', 'fr', 'de']
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
maxMessageLength: 2000,
|
||||
allowFileUpload: true,
|
||||
allowVoiceMessage: false,
|
||||
autoScroll: true
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSpecialistComplete(eventData) {
|
||||
// Wordt binnen CoreChatApp afgehandeld; deze shell kan in de toekomst
|
||||
// aanvullende UI-reacties toevoegen.
|
||||
this.$emit('specialist-complete', eventData);
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
this.$emit('specialist-error', eventData);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desktop-chat-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
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;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -20,8 +20,8 @@
|
||||
:api-prefix="apiPrefix"
|
||||
:is-latest-ai-message="isLatestAiMessage(message)"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
@specialist-complete="handleSpecialistCompleteFromMessage"
|
||||
@specialist-error="handleSpecialistErrorFromMessage"
|
||||
></chat-message>
|
||||
</template>
|
||||
</template>
|
||||
@@ -173,6 +173,15 @@ export default {
|
||||
if (this._resizeObserver) this._resizeObserver.disconnect();
|
||||
},
|
||||
methods: {
|
||||
handleSpecialistCompleteFromMessage(eventData) {
|
||||
console.log('🧐 [MessageHistory] specialist-complete ontvangen van ChatMessage, bubbelt naar parent:', eventData);
|
||||
this.$emit('specialist-complete', eventData);
|
||||
},
|
||||
|
||||
handleSpecialistErrorFromMessage(eventData) {
|
||||
console.log('🧐 [MessageHistory] specialist-error ontvangen van ChatMessage, bubbelt naar parent:', eventData);
|
||||
this.$emit('specialist-error', eventData);
|
||||
},
|
||||
async handleLanguageChange(event) {
|
||||
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
||||
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<!--
|
||||
MobileChatShell
|
||||
----------------
|
||||
Shell voor mobiele schermen. Biedt een header met logo + tabbar en toont
|
||||
per tab een deel van de CoreChatApp:
|
||||
- chat: actieve AI-boodschap + invoer
|
||||
- history: volledige berichtenhistoriek
|
||||
- setup: taalkeuze + uitleg
|
||||
-->
|
||||
<div class="mobile-chat-shell">
|
||||
<header class="chat-mobile-header">
|
||||
<div class="chat-mobile-header-left">
|
||||
<SideBarLogo :logo-url="tenantLogoUrl" :make-name="tenantName" />
|
||||
</div>
|
||||
<div class="chat-mobile-header-right">
|
||||
<MobileTabBar
|
||||
v-model="activeTabId"
|
||||
:tabs="mobileTabs"
|
||||
placement="header"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="chat-mobile-content" :class="`tab-${activeTabId}`">
|
||||
<CoreChatApp
|
||||
:api-prefix="apiPrefix"
|
||||
:conversation-id="conversationId"
|
||||
:user-id="userId"
|
||||
:user-name="userName"
|
||||
:initial-language="initialLanguage"
|
||||
:supported-language-details="supportedLanguageDetails"
|
||||
:allowed-languages="allowedLanguages"
|
||||
>
|
||||
<!-- Historiek-tab -->
|
||||
<template #history="historyProps">
|
||||
<MessageHistory
|
||||
v-if="activeTabId === 'history'"
|
||||
:messages="historyProps.messages"
|
||||
:is-typing="historyProps.isTyping"
|
||||
:is-submitting-form="historyProps.isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:auto-scroll="true"
|
||||
@specialist-error="event => {
|
||||
console.log('🧐 [MobileChatShell] specialist-error vanuit history ontvangen:', event);
|
||||
(historyProps.onSpecialistError || handleSpecialistError)(event);
|
||||
}"
|
||||
@specialist-complete="event => {
|
||||
console.log('🧐 [MobileChatShell] specialist-complete vanuit history ontvangen:', event);
|
||||
(historyProps.onSpecialistComplete || handleSpecialistComplete)(event);
|
||||
}"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Chat-tab: actieve boodschap + input -->
|
||||
<template #active-message-input="inputProps">
|
||||
<ChatInput
|
||||
v-if="activeTabId === 'chat'"
|
||||
:current-message="inputProps.currentMessage"
|
||||
:is-loading="inputProps.isLoading"
|
||||
:max-length="settings.maxMessageLength"
|
||||
:allow-file-upload="settings.allowFileUpload"
|
||||
:allow-voice-message="settings.allowVoiceMessage"
|
||||
:form-data="inputProps.formData"
|
||||
:active-ai-message="inputProps.activeAiMessage"
|
||||
:api-prefix="apiPrefix"
|
||||
@send-message="inputProps.onSendMessage"
|
||||
@update-message="inputProps.onUpdateMessage"
|
||||
@upload-file="inputProps.onUploadFile"
|
||||
@record-voice="inputProps.onRecordVoice"
|
||||
@submit-form="inputProps.onSubmitForm"
|
||||
@specialist-error="event => {
|
||||
console.log('🧐 [MobileChatShell] specialist-error vanuit ChatInput ontvangen:', event);
|
||||
(inputProps.onSpecialistError || handleSpecialistError)(event);
|
||||
}"
|
||||
@specialist-complete="event => {
|
||||
console.log('🧐 [MobileChatShell] specialist-complete vanuit ChatInput ontvangen:', event);
|
||||
(inputProps.onSpecialistComplete || handleSpecialistComplete)(event);
|
||||
}"
|
||||
ref="chatInput"
|
||||
class="chat-input-area tab-chat-input"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Setup-tab -->
|
||||
<template #setup="setupProps">
|
||||
<SideBarMobileSetup
|
||||
v-if="activeTabId === 'setup'"
|
||||
:tenant-make="{ name: tenantName, subtitle: tenantSubtitle }"
|
||||
:explanation-text="setupProps.explanationText || explanationText"
|
||||
:initial-language="initialLanguage"
|
||||
:current-language="setupProps.currentLanguage || currentLanguage"
|
||||
:supported-language-details="supportedLanguageDetails"
|
||||
:allowed-languages="allowedLanguages"
|
||||
:api-prefix="apiPrefix"
|
||||
@language-changed="handleLanguageChangedFromSetup"
|
||||
/>
|
||||
</template>
|
||||
</CoreChatApp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CoreChatApp from './CoreChatApp.vue';
|
||||
import MessageHistory from './MessageHistory.vue';
|
||||
import ChatInput from './ChatInput.vue';
|
||||
import SideBarLogo from './SideBarLogo.vue';
|
||||
import MobileTabBar from './MobileTabBar.vue';
|
||||
import SideBarMobileSetup from './SideBarMobileSetup.vue';
|
||||
|
||||
export default {
|
||||
name: 'MobileChatShell',
|
||||
|
||||
components: {
|
||||
CoreChatApp,
|
||||
MessageHistory,
|
||||
ChatInput,
|
||||
SideBarLogo,
|
||||
MobileTabBar,
|
||||
SideBarMobileSetup
|
||||
},
|
||||
|
||||
props: {
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
userId: {
|
||||
type: [String, Number, null],
|
||||
default: null
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialLanguage: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
supportedLanguageDetails: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowedLanguages: {
|
||||
type: Array,
|
||||
default: () => ['nl', 'en', 'fr', 'de']
|
||||
},
|
||||
tenantName: {
|
||||
type: String,
|
||||
default: 'EveAI'
|
||||
},
|
||||
tenantSubtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tenantLogoUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
explanationText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
maxMessageLength: 2000,
|
||||
allowFileUpload: true,
|
||||
allowVoiceMessage: false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeTabId: 'chat',
|
||||
currentLanguage: this.initialLanguage || 'en'
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
mobileTabs() {
|
||||
return [
|
||||
{ id: 'chat', iconName: 'chat', label: 'Chat' },
|
||||
{ id: 'history', iconName: 'history', label: 'Historiek' },
|
||||
{ id: 'setup', iconName: 'settings', label: 'Setup' }
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Initiale tab kan later uit config komen (defaultTab), voorlopig chat.
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSpecialistComplete(eventData) {
|
||||
this.$emit('specialist-complete', eventData);
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
this.$emit('specialist-error', eventData);
|
||||
},
|
||||
|
||||
handleLanguageChangedFromSetup(newLanguage) {
|
||||
this.currentLanguage = newLanguage;
|
||||
this.$emit('language-changed', newLanguage);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-chat-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-mobile-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
background: var(--tab-background, #0a0a0a);
|
||||
}
|
||||
|
||||
.chat-mobile-header-left {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-width: 56px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.chat-mobile-header-right {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-mobile-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-mobile-content.tab-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-chat-input {
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
padding-bottom: calc(6px + var(--safe-bottom-inset, 0px));
|
||||
}
|
||||
|
||||
body.chat-keyboard-open .tab-chat-input {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -27,8 +27,10 @@ console.log('Components loaded:', Object.keys(Components));
|
||||
|
||||
// Import specifieke componenten
|
||||
import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue';
|
||||
import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue';
|
||||
// import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue';
|
||||
import ChatRoot from '../../../eveai_chat_client/static/assets/vue-components/ChatRoot.vue';
|
||||
import DesktopChatShell from '../../../eveai_chat_client/static/assets/vue-components/DesktopChatShell.vue';
|
||||
import MobileChatShell from '../../../eveai_chat_client/static/assets/vue-components/MobileChatShell.vue';
|
||||
import SideBar from '../../../eveai_chat_client/static/assets/vue-components/SideBar.vue';
|
||||
|
||||
// VueUse-setup voor de chatclient (maakt composables beschikbaar via window.VueUse)
|
||||
@@ -131,27 +133,43 @@ function initializeChatApp() {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!ChatApp) {
|
||||
throw new Error('🚨 [CRITICAL ERROR] ChatApp component niet gevonden');
|
||||
}
|
||||
|
||||
// Extra verificatie dat alle sub-componenten beschikbaar zijn
|
||||
if (!Components.MessageHistory || !Components.ChatInput ||
|
||||
!Components.TypingIndicator || !Components.ChatMessage) {
|
||||
console.warn('⚠️ [WARN] Niet alle benodigde sub-componenten zijn geladen!');
|
||||
}
|
||||
|
||||
// Maak props voor de component
|
||||
const props = {
|
||||
// Maak props voor de shells / CoreChatApp
|
||||
const baseProps = {
|
||||
apiPrefix: window.chatConfig.apiPrefix || '',
|
||||
conversationId: window.chatConfig.conversationId || 'default',
|
||||
userId: window.chatConfig.userId || null,
|
||||
userName: window.chatConfig.userName || '',
|
||||
initialLanguage: window.chatConfig.language || 'nl',
|
||||
supportedLanguageDetails: window.chatConfig.supportedLanguageDetails || {},
|
||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de']
|
||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
||||
tenantName: (window.chatConfig.tenantMake && window.chatConfig.tenantMake.name) || 'EveAI',
|
||||
tenantSubtitle: (window.chatConfig.tenantMake && window.chatConfig.tenantMake.subtitle) || '',
|
||||
tenantLogoUrl: (window.chatConfig.tenantMake && window.chatConfig.tenantMake.logo_url) || '',
|
||||
explanationText: window.chatConfig.explanation || '',
|
||||
settings: window.chatConfig.settings || {}
|
||||
};
|
||||
|
||||
// Bepaal shell-type: expliciete config heeft voorrang, anders breakpoint
|
||||
const layoutMode = window.chatConfig.layoutMode || 'auto';
|
||||
const isMobileBreakpoint = window.innerWidth <= 768;
|
||||
let ShellComponent;
|
||||
|
||||
if (layoutMode === 'desktop') {
|
||||
ShellComponent = DesktopChatShell;
|
||||
} else if (layoutMode === 'mobile') {
|
||||
ShellComponent = MobileChatShell;
|
||||
} else {
|
||||
ShellComponent = isMobileBreakpoint ? MobileChatShell : DesktopChatShell;
|
||||
}
|
||||
|
||||
const props = { shellComponent: ShellComponent, shellProps: baseProps };
|
||||
|
||||
// Mount de component via ChatRoot zodat SafeViewport de layout kan beheren
|
||||
const app = createApp(ChatRoot, props);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user