Merge branch 'release/v3.1.36-beta'
This commit is contained in:
@@ -36,7 +36,10 @@ def get_default_chat_customisation(tenant_customisation=None):
|
|||||||
'ai_message_text_color': '#212529',
|
'ai_message_text_color': '#212529',
|
||||||
'human_message_background': '#212529',
|
'human_message_background': '#212529',
|
||||||
'human_message_text_color': '#ffffff',
|
'human_message_text_color': '#ffffff',
|
||||||
'human_message_inactive_text_color': '#808080'
|
'human_message_inactive_text_color': '#808080',
|
||||||
|
'tab_background': '#0a0a0a',
|
||||||
|
'tab_icon_active_color': '#ffffff',
|
||||||
|
'tab_icon_inactive_color': '#f0f0f0',
|
||||||
}
|
}
|
||||||
|
|
||||||
# If no tenant customization is provided, return the defaults
|
# If no tenant customization is provided, return the defaults
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ configuration:
|
|||||||
description: "Human Message Inactive Text Color"
|
description: "Human Message Inactive Text Color"
|
||||||
type: "color"
|
type: "color"
|
||||||
required: false
|
required: false
|
||||||
|
tab_background:
|
||||||
|
name: "Tab Background Color"
|
||||||
|
description: "Tab Background Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
tab_icon_active_color:
|
||||||
|
name: "Tab Icon Active Color"
|
||||||
|
description: "Tab Icon Active Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
tab_icon_inactive_color:
|
||||||
|
name: "Tab Icon Inactive Color"
|
||||||
|
description: "Tab Icon Inactive Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
metadata:
|
metadata:
|
||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2024-06-06"
|
date_added: "2024-06-06"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dist/chat-client.js": "dist/chat-client.f8ee4d5a.js",
|
"dist/chat-client.js": "dist/chat-client.825210dd.js",
|
||||||
"dist/chat-client.css": "dist/chat-client.2fffefae.css",
|
"dist/chat-client.css": "dist/chat-client.568d7be7.css",
|
||||||
"dist/main.js": "dist/main.6a617099.js",
|
"dist/main.js": "dist/main.6a617099.js",
|
||||||
"dist/main.css": "dist/main.7182aac3.css"
|
"dist/main.css": "dist/main.7182aac3.css"
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,22 @@ task_description: >
|
|||||||
€€€{history}€€€
|
€€€{history}€€€
|
||||||
|
|
||||||
(In this history, user interactions are preceded by 'HUMAN', and your interactions with 'AI'.)
|
(In this history, user interactions are preceded by 'HUMAN', and your interactions with 'AI'.)
|
||||||
|
Take into account the last question asked by the you, the AI.
|
||||||
|
|
||||||
Check if the user has given an affirmative answer or not.
|
Check if the user has given an affirmative answer to that last question or not.
|
||||||
Please note that this answer can be very short:
|
Please note that this answer can be very short:
|
||||||
- Affirmative answers: e.g. Yes, OK, Sure, Of Course
|
- Affirmative answers: e.g. Yes, OK, Sure, Of Course
|
||||||
- Negative answers: e.g. No, not really, No, I'd rather not.
|
- Negative answers: e.g. No, not really, No, I'd rather not.
|
||||||
|
Also note that users may use emoticons, emojis, or other symbols to express their affirmative answers.
|
||||||
|
- Affirmative answers: e.g. 👍🏼 , 👌🏼 , ☺️
|
||||||
|
- Negative answers: e.g. 👎🏼 , 🙅🏼 , 😒
|
||||||
|
Finally, users may use a direct answer to the last question asked:
|
||||||
|
Example 1:
|
||||||
|
- Question: "Do you have any other questions, or shall we start the interview to see if there’s a match with the job?"
|
||||||
|
- Affirmative Answer: "Start the interview" or "Start please"
|
||||||
|
Example 2:
|
||||||
|
- Question: "Is there anything still on your mind, or shall we begin the conversation to explore the match?"
|
||||||
|
- Affirmative Answer: "Let's start exploring" or "Let's go"
|
||||||
|
|
||||||
Please consider that the answer will be given in {language}!
|
Please consider that the answer will be given in {language}!
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,24 @@ All notable changes to EveAI will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 3.1.36-beta
|
||||||
|
|
||||||
|
Release date: 2025-12-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Refactoring of the chat client to use tabs for the active conversation, history and settings.
|
||||||
|
- Introduction of shells for Mobile and Desktop clients, allowing for additional shells like plugins to be added in the future.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- TRA-89—Problem solved where connection could get lost in sync between client and backend
|
||||||
|
- TRA-98—End user could continue without accepting dpa & terms
|
||||||
|
- TRA-96—Multiple-choice questions in the mobile client not scrolling → Solved by introducing a new client layout
|
||||||
|
- TRA-101—DPA-link was not working
|
||||||
|
- TRA-102—Wrong responses when looking for affirmative answers.
|
||||||
|
|
||||||
## 3.1.26-beta
|
## 3.1.26-beta
|
||||||
|
|
||||||
Release date:
|
Release date: 2025-11-26
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Introduction of vueuse/core in the chat client, to ensure abstraction of ui behaviour for different mobile devices.
|
- Introduction of vueuse/core in the chat client, to ensure abstraction of ui behaviour for different mobile devices.
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
/* App container layout */
|
/* App container layout */
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* Use visual viewport variable when available */
|
/* Op desktop gebruiken we de veilige viewporthoogte direct; op mobiel
|
||||||
|
laten we html/body de hoogte bepalen en neemt de app-container
|
||||||
|
eenvoudig 100% daarvan in via de media query verderop. */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: auto; /* prefer dynamic viewport on desktop */
|
height: auto; /* desktop: dynamische hoogte, op mobiel overschreven */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
@@ -103,6 +105,26 @@
|
|||||||
min-height: 0; /* laat kinderen (ChatApp) krimpen */
|
min-height: 0; /* laat kinderen (ChatApp) krimpen */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Op mobiel sluiten we de volledige content-kolom strak aan op de veilige
|
||||||
|
viewporthoogte zodat alleen de chatcontent zelf kan scrollen en niet de
|
||||||
|
gehele pagina wanneer het toetsenbord opent. */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -1,38 +1,106 @@
|
|||||||
active_text_color<template>
|
active_text_color<template>
|
||||||
<div class="chat-app-container">
|
<div class="chat-app-container">
|
||||||
<!-- Message History - takes available space -->
|
<!-- Desktop layout: huidige gedrag behouden -->
|
||||||
<message-history
|
<div v-if="!isMobileFallback" class="chat-desktop-layout">
|
||||||
:messages="displayMessages"
|
<message-history
|
||||||
:is-typing="isTyping"
|
:messages="displayMessages"
|
||||||
:is-submitting-form="isSubmittingForm"
|
:is-typing="isTyping"
|
||||||
:api-prefix="apiPrefix"
|
:is-submitting-form="isSubmittingForm"
|
||||||
:auto-scroll="true"
|
:api-prefix="apiPrefix"
|
||||||
@specialist-error="handleSpecialistError"
|
:auto-scroll="true"
|
||||||
@specialist-complete="handleSpecialistComplete"
|
@specialist-error="handleSpecialistError"
|
||||||
ref="messageHistory"
|
@specialist-complete="handleSpecialistComplete"
|
||||||
class="chat-messages-area"
|
ref="messageHistory"
|
||||||
></message-history>
|
class="chat-messages-area"
|
||||||
|
></message-history>
|
||||||
|
|
||||||
<!-- Chat Input - to the bottom -->
|
<chat-input
|
||||||
<chat-input
|
:current-message="currentMessage"
|
||||||
:current-message="currentMessage"
|
:is-loading="isLoading"
|
||||||
:is-loading="isLoading"
|
:max-length="2000"
|
||||||
:max-length="2000"
|
:allow-file-upload="true"
|
||||||
:allow-file-upload="true"
|
:allow-voice-message="false"
|
||||||
:allow-voice-message="false"
|
:form-data="currentInputFormData"
|
||||||
:form-data="currentInputFormData"
|
:active-ai-message="activeAiMessage"
|
||||||
:active-ai-message="activeAiMessage"
|
:api-prefix="apiPrefix"
|
||||||
:api-prefix="apiPrefix"
|
@send-message="sendMessage"
|
||||||
@send-message="sendMessage"
|
@update-message="updateCurrentMessage"
|
||||||
@update-message="updateCurrentMessage"
|
@upload-file="handleFileUpload"
|
||||||
@upload-file="handleFileUpload"
|
@record-voice="handleVoiceRecord"
|
||||||
@record-voice="handleVoiceRecord"
|
@submit-form="submitFormFromInput"
|
||||||
@submit-form="submitFormFromInput"
|
@specialist-error="handleSpecialistError"
|
||||||
@specialist-error="handleSpecialistError"
|
@specialist-complete="handleSpecialistComplete"
|
||||||
@specialist-complete="handleSpecialistComplete"
|
ref="chatInput"
|
||||||
ref="chatInput"
|
class="chat-input-area"
|
||||||
class="chat-input-area"
|
></chat-input>
|
||||||
></chat-input>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobiele layout met tabs in de header -->
|
||||||
|
<div v-else class="chat-mobile-layout">
|
||||||
|
<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}`">
|
||||||
|
<message-history
|
||||||
|
v-if="activeTabId === '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
|
||||||
|
v-else-if="activeTabId === 'chat'"
|
||||||
|
:current-message="currentMessage"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:max-length="settings.maxMessageLength"
|
||||||
|
:allow-file-upload="settings.allowFileUpload"
|
||||||
|
:allow-voice-message="settings.allowVoiceMessage"
|
||||||
|
: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 tab-chat-input"
|
||||||
|
></chat-input>
|
||||||
|
|
||||||
|
<SideBarMobileSetup
|
||||||
|
v-else-if="activeTabId === 'setup'"
|
||||||
|
:tenant-make="{ name: tenantName, subtitle: tenantSubtitle }"
|
||||||
|
:explanation-text="originalExplanation"
|
||||||
|
:initial-language="currentLanguage"
|
||||||
|
:current-language="currentLanguage"
|
||||||
|
:supported-language-details="supportedLanguageDetails"
|
||||||
|
:allowed-languages="allowedLanguages"
|
||||||
|
:api-prefix="apiPrefix"
|
||||||
|
@language-changed="handleLanguageChangedFromSetup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content Modal - positioned at ChatApp level -->
|
<!-- Content Modal - positioned at ChatApp level -->
|
||||||
<content-modal
|
<content-modal
|
||||||
@@ -60,6 +128,9 @@ import ProgressTracker from './ProgressTracker.vue';
|
|||||||
import LanguageSelector from './LanguageSelector.vue';
|
import LanguageSelector from './LanguageSelector.vue';
|
||||||
import ChatInput from './ChatInput.vue';
|
import ChatInput from './ChatInput.vue';
|
||||||
import ContentModal from './ContentModal.vue';
|
import ContentModal from './ContentModal.vue';
|
||||||
|
import SideBarLogo from './SideBarLogo.vue';
|
||||||
|
import MobileTabBar from './MobileTabBar.vue';
|
||||||
|
import SideBarMobileSetup from './SideBarMobileSetup.vue';
|
||||||
|
|
||||||
// Import language provider
|
// Import language provider
|
||||||
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
|
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
|
||||||
@@ -77,7 +148,10 @@ export default {
|
|||||||
MessageHistory,
|
MessageHistory,
|
||||||
ProgressTracker,
|
ProgressTracker,
|
||||||
ChatInput,
|
ChatInput,
|
||||||
ContentModal
|
ContentModal,
|
||||||
|
SideBarLogo,
|
||||||
|
MobileTabBar,
|
||||||
|
SideBarMobileSetup
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
@@ -111,6 +185,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// Tenant info
|
// Tenant info
|
||||||
tenantName: tenantMake.name || 'EveAI',
|
tenantName: tenantMake.name || 'EveAI',
|
||||||
|
tenantSubtitle: tenantMake.subtitle || '',
|
||||||
tenantLogoUrl: tenantMake.logo_url || '',
|
tenantLogoUrl: tenantMake.logo_url || '',
|
||||||
|
|
||||||
// Taal gerelateerde data
|
// Taal gerelateerde data
|
||||||
@@ -147,10 +222,13 @@ export default {
|
|||||||
autoScroll: settings.autoScroll === true
|
autoScroll: settings.autoScroll === true
|
||||||
},
|
},
|
||||||
|
|
||||||
// UI state
|
// UI state (fallback flags voor oudere logica)
|
||||||
isMobile: window.innerWidth <= 768,
|
isMobileFallback: window.innerWidth <= 768,
|
||||||
showSidebar: window.innerWidth > 768,
|
showSidebar: window.innerWidth > 768,
|
||||||
|
|
||||||
|
// Mobile tab state
|
||||||
|
activeTabId: 'chat',
|
||||||
|
|
||||||
// Advanced features
|
// Advanced features
|
||||||
messageSearch: '',
|
messageSearch: '',
|
||||||
filteredMessages: [],
|
filteredMessages: [],
|
||||||
@@ -193,16 +271,57 @@ export default {
|
|||||||
return this.supportedLanguages.filter(lang =>
|
return this.supportedLanguages.filter(lang =>
|
||||||
this.allowedLanguages.includes(lang.code)
|
this.allowedLanguages.includes(lang.code)
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
mobileTabs() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'chat',
|
||||||
|
iconName: 'chat',
|
||||||
|
label: 'Chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'history',
|
||||||
|
iconName: 'history',
|
||||||
|
label: 'Historiek'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'setup',
|
||||||
|
iconName: 'settings',
|
||||||
|
label: 'Setup'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initializeChat();
|
this.initializeChat();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Stel initiale actieve tab in (optioneel via config)
|
||||||
|
const defaultTab = (window.chatConfig && window.chatConfig.defaultTab) || 'chat';
|
||||||
|
if (this.mobileTabs.find(t => t.id === defaultTab)) {
|
||||||
|
this.activeTabId = defaultTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luister naar globale events om tab te wisselen
|
||||||
|
this.globalTabListener = (event) => {
|
||||||
|
const tabId = event?.detail?.tabId;
|
||||||
|
if (!tabId) return;
|
||||||
|
if (this.mobileTabs.find(t => t.id === tabId)) {
|
||||||
|
this.activeTabId = tabId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('evie-chat-set-tab', this.globalTabListener);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
|
if (this.globalTabListener) {
|
||||||
|
document.removeEventListener('evie-chat-set-tab', this.globalTabListener);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -453,11 +572,13 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Window resize listener
|
// Window resize listener voor fallback flags
|
||||||
window.addEventListener('resize', () => {
|
this.handleResize = () => {
|
||||||
this.isMobile = window.innerWidth <= 768;
|
this.isMobileFallback = window.innerWidth <= 768;
|
||||||
this.showSidebar = window.innerWidth > 768;
|
this.showSidebar = window.innerWidth > 768;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
@@ -516,6 +637,12 @@ export default {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleLanguageChangedFromSetup(newLanguage) {
|
||||||
|
// Update lokale taalstate; verdere effecten worden opgepikt door
|
||||||
|
// bestaande global listener en LanguageProvider / chatConfig.
|
||||||
|
this.currentLanguage = newLanguage;
|
||||||
|
},
|
||||||
|
|
||||||
// UI helpers
|
// UI helpers
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
if (this.$refs.messageHistory) {
|
if (this.$refs.messageHistory) {
|
||||||
@@ -560,7 +687,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
/* height: 100%; avoided to let flex sizing control height */
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
@@ -571,6 +698,84 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-mobile-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* Binnen SafeViewport vertrouwen we op de hoogte van de bovenliggende
|
||||||
|
containers (html/body/app-container). Deze layout moet zich daaraan
|
||||||
|
aanpassen en niet opnieuw zelf een safe-vh berekening doen, om
|
||||||
|
dubbele afrondingsfouten en extra scrollruimte te vermijden. */
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-mobile-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobiele header met logo links en tabs rechts */
|
||||||
|
.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; /* Zorg dat logo altijd minstens vierkant kan tonen */
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-mobile-header-right {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specifieke layout voor de chat-tab: inputblok onderaan */
|
||||||
|
.chat-mobile-content.tab-chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-chat-input {
|
||||||
|
margin-top: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Iets meer visuele marge boven de onderrand, maar nog steeds rekening houden met safe inset */
|
||||||
|
padding-bottom: calc(6px + var(--safe-bottom-inset, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wanneer het toetsenbord open is (gedetecteerd door useChatViewport via
|
||||||
|
de body-klasse chat-keyboard-open), willen we geen extra grote
|
||||||
|
safe-bottom-inset meer onder de input. Dan sluiten we zo veel mogelijk
|
||||||
|
aan tegen de visuele viewport en houden we alleen een kleine vaste marge. */
|
||||||
|
body.chat-keyboard-open .tab-chat-input {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-app-container {
|
||||||
|
/* Minder padding op mobiel zodat de tabbar binnen de viewport valt */
|
||||||
|
padding: 8px 8px 0 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-messages-area {
|
.chat-messages-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0; /* ensure child can scroll */
|
min-height: 0; /* ensure child can scroll */
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
:is-latest-ai-message="true"
|
:is-latest-ai-message="true"
|
||||||
:is-in-input-area="true"
|
:is-in-input-area="true"
|
||||||
@image-loaded="handleImageLoaded"
|
@image-loaded="handleImageLoaded"
|
||||||
@specialist-complete="$emit('specialist-complete', $event)"
|
@specialist-complete="handleSpecialistCompleteFromActiveMessage"
|
||||||
@specialist-error="$emit('specialist-error', $event)"
|
@specialist-error="handleSpecialistErrorFromActiveMessage"
|
||||||
></chat-message>
|
></chat-message>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -183,22 +183,22 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
formData: {
|
formData: {
|
||||||
handler(newFormData, oldFormData) {
|
handler(newFormData, oldFormData) {
|
||||||
console.log('ChatInput formData changed:', newFormData);
|
console.log('🧐 [ChatInput] formData changed:', newFormData);
|
||||||
|
|
||||||
if (!newFormData) {
|
if (!newFormData) {
|
||||||
console.log('FormData is null of undefined');
|
console.log('🧐 [ChatInput] formData is null of undefined');
|
||||||
this.formValues = {};
|
this.formValues = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controleer of velden aanwezig zijn
|
// Controleer of velden aanwezig zijn
|
||||||
if (!newFormData.fields) {
|
if (!newFormData.fields) {
|
||||||
console.error('FormData bevat geen velden!', newFormData);
|
console.error('🧐 [ChatInput] formData bevat geen velden!', newFormData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Velden in formData:', newFormData.fields);
|
console.log('🧐 [ChatInput] velden in formData:', newFormData.fields);
|
||||||
console.log('Aantal velden:', Array.isArray(newFormData.fields)
|
console.log('🧐 [ChatInput] aantal velden:', Array.isArray(newFormData.fields)
|
||||||
? newFormData.fields.length
|
? newFormData.fields.length
|
||||||
: Object.keys(newFormData.fields).length);
|
: Object.keys(newFormData.fields).length);
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ export default {
|
|||||||
this.initFormValues();
|
this.initFormValues();
|
||||||
|
|
||||||
// Log de geïnitialiseerde waarden
|
// Log de geïnitialiseerde waarden
|
||||||
console.log('Formulierwaarden geïnitialiseerd:', this.formValues);
|
console.log('🧐 [ChatInput] formulierwaarden geïnitialiseerd:', this.formValues);
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
deep: true
|
deep: true
|
||||||
@@ -251,6 +251,15 @@ export default {
|
|||||||
window.removeEventListener('resize', this.autoResize);
|
window.removeEventListener('resize', this.autoResize);
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
handleLanguageChange(event) {
|
||||||
if (event.detail && event.detail.language) {
|
if (event.detail && event.detail.language) {
|
||||||
this.translatePlaceholder(event.detail.language);
|
this.translatePlaceholder(event.detail.language);
|
||||||
@@ -452,6 +461,7 @@ export default {
|
|||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input veld en knoppen */
|
/* Input veld en knoppen */
|
||||||
|
|||||||
@@ -784,21 +784,32 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bubble height constraints and inner scroll containment (apply on all viewports) */
|
/* Bubble height constraints en inner scroll containment.
|
||||||
|
Fallback gebruikt klassieke vh-units; de @supports-blok hieronder
|
||||||
|
schakelt over naar SafeViewport via var(--safe-vh) wanneer mogelijk. */
|
||||||
.message .message-content {
|
.message .message-content {
|
||||||
max-height: 33vh; /* fallback */
|
max-height: 33vh; /* fallback */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
||||||
-webkit-overflow-scrolling: touch; /* iOS smooth inertia */
|
-webkit-overflow-scrolling: touch; /* iOS smooth inertia */
|
||||||
}
|
}
|
||||||
/* Active contexts (input area or sticky area): allow up to half viewport */
|
|
||||||
|
/* Active contexts (input area of sticky area): mogen meer hoogte innemen */
|
||||||
.message.input-area .message-content,
|
.message.input-area .message-content,
|
||||||
.message.sticky-area .message-content {
|
.message.sticky-area .message-content {
|
||||||
max-height: 50vh; /* fallback */
|
max-height: 50vh; /* fallback */
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports (max-height: 1svh) {
|
@supports (max-height: 1svh) {
|
||||||
.message .message-content { max-height: 33svh; }
|
.message .message-content {
|
||||||
.message.input-area .message-content,
|
/* Gebruik veilige viewporthoogte die door useChatViewport gezet wordt */
|
||||||
.message.sticky-area .message-content { max-height: 50svh; }
|
max-height: calc(var(--safe-vh, 1vh) * 33);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.input-area .message-content,
|
||||||
|
.message.sticky-area .message-content {
|
||||||
|
/* In de input-/sticky-area mag de bubbel ruimer zijn */
|
||||||
|
max-height: calc(var(--safe-vh, 1vh) * 60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,50 @@
|
|||||||
<template>
|
<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>
|
<SafeViewport>
|
||||||
<ChatApp />
|
<component :is="shellComponent" v-bind="shellProps" />
|
||||||
</SafeViewport>
|
</SafeViewport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// ChatRoot.vue
|
import { computed } from 'vue';
|
||||||
// Kleine root-component die de ChatApp binnen de SafeViewport wrapper rendert.
|
|
||||||
|
|
||||||
import ChatApp from './ChatApp.vue';
|
|
||||||
import SafeViewport from './SafeViewport.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>
|
</script>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default {
|
|||||||
const source = (this.template || '');
|
const source = (this.template || '');
|
||||||
|
|
||||||
// 2) parse only allowed tags <dpa>...</dpa> and <terms>...</terms>
|
// 2) parse only allowed tags <dpa>...</dpa> and <terms>...</terms>
|
||||||
const pattern = /<(privacy|terms)>([\s\S]*?)<\/\1>/gi;
|
const pattern = /<(dpa|terms)>([\s\S]*?)<\/\1>/gi;
|
||||||
const out = [];
|
const out = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
@@ -62,8 +62,17 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
emitClick(kind) {
|
emitClick(kind) {
|
||||||
if (kind === 'dpa') this.$emit('open-dpa');
|
// Debug logging to trace click events for consent links
|
||||||
if (kind === 'terms') this.$emit('open-terms');
|
console.log('[ConsentRichText] emitClick called with kind =', kind);
|
||||||
|
|
||||||
|
if (kind === 'dpa') {
|
||||||
|
console.log('[ConsentRichText] Emitting open-dpa event');
|
||||||
|
this.$emit('open-dpa');
|
||||||
|
}
|
||||||
|
if (kind === 'terms') {
|
||||||
|
console.log('[ConsentRichText] Emitting open-terms event');
|
||||||
|
this.$emit('open-terms');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
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>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
:field-id="field.id || field.name"
|
:field-id="field.id || field.name"
|
||||||
:model-value="localFormValues[field.id || field.name]"
|
:model-value="localFormValues[field.id || field.name]"
|
||||||
@update:model-value="updateFieldValue(field.id || field.name, $event)"
|
@update:model-value="updateFieldValue(field.id || field.name, $event)"
|
||||||
@open-privacy-modal="openPrivacyModal"
|
@open-dpa-modal="openDpaModal"
|
||||||
@open-terms-modal="openTermsModal"
|
@open-terms-modal="openTermsModal"
|
||||||
@keydown-enter="handleEnterKey"
|
@keydown-enter="handleEnterKey"
|
||||||
/>
|
/>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
:field-id="fieldId"
|
:field-id="fieldId"
|
||||||
:model-value="localFormValues[fieldId]"
|
:model-value="localFormValues[fieldId]"
|
||||||
@update:model-value="updateFieldValue(fieldId, $event)"
|
@update:model-value="updateFieldValue(fieldId, $event)"
|
||||||
@open-privacy-modal="openPrivacyModal"
|
@open-dpa-modal="openDpaModal"
|
||||||
@open-terms-modal="openTermsModal"
|
@open-terms-modal="openTermsModal"
|
||||||
@keydown-enter="handleEnterKey"
|
@keydown-enter="handleEnterKey"
|
||||||
/>
|
/>
|
||||||
@@ -199,12 +199,19 @@ export default {
|
|||||||
// Basic validation - check required fields
|
// Basic validation - check required fields
|
||||||
const missingFields = [];
|
const missingFields = [];
|
||||||
|
|
||||||
|
// Extra consent-validatie: detecteer consent velden en controleer of alle consents geaccepteerd zijn.
|
||||||
|
// We maken dit toekomstvast voor meerdere consent-velden.
|
||||||
|
let hasConsentField = false;
|
||||||
|
let allConsentsAccepted = true;
|
||||||
|
|
||||||
if (Array.isArray(this.formData.fields)) {
|
if (Array.isArray(this.formData.fields)) {
|
||||||
// Valideer array-gebaseerde velden
|
// Valideer array-gebaseerde velden
|
||||||
this.formData.fields.forEach(field => {
|
this.formData.fields.forEach(field => {
|
||||||
const fieldId = field.id || field.name;
|
const fieldId = field.id || field.name;
|
||||||
|
const value = this.localFormValues[fieldId];
|
||||||
|
|
||||||
|
// Basis required-validatie
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
const value = this.localFormValues[fieldId];
|
|
||||||
// Voor boolean velden is false een geldige waarde
|
// Voor boolean velden is false een geldige waarde
|
||||||
if (field.type === 'boolean') {
|
if (field.type === 'boolean') {
|
||||||
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
@@ -220,12 +227,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consent-detectie en -validatie (ongeacht required-vlag)
|
||||||
|
if (field.type === 'boolean' && field.meta && field.meta.kind === 'consent') {
|
||||||
|
hasConsentField = true;
|
||||||
|
if (value !== true) {
|
||||||
|
allConsentsAccepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Valideer object-gebaseerde velden
|
// Valideer object-gebaseerde velden
|
||||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||||
|
const value = this.localFormValues[fieldId];
|
||||||
|
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
const value = this.localFormValues[fieldId];
|
|
||||||
// Voor boolean velden is false een geldige waarde
|
// Voor boolean velden is false een geldige waarde
|
||||||
if (field.type === 'boolean') {
|
if (field.type === 'boolean') {
|
||||||
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
// Boolean velden zijn altijd geldig als ze een boolean waarde hebben
|
||||||
@@ -241,10 +257,27 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consent-detectie en -validatie (ongeacht required-vlag)
|
||||||
|
if (field.type === 'boolean' && field.meta && field.meta.kind === 'consent') {
|
||||||
|
hasConsentField = true;
|
||||||
|
if (value !== true) {
|
||||||
|
allConsentsAccepted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return missingFields.length === 0;
|
const isBaseValid = missingFields.length === 0;
|
||||||
|
|
||||||
|
if (!hasConsentField) {
|
||||||
|
// Geen speciale consentvelden: behoud bestaand gedrag
|
||||||
|
return isBaseValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als er één of meer consentvelden zijn, zijn we alleen geldig als
|
||||||
|
// zowel de basisvalidatie als alle consents geaccepteerd zijn.
|
||||||
|
return isBaseValid && allConsentsAccepted;
|
||||||
},
|
},
|
||||||
// Title display mode configuration
|
// Title display mode configuration
|
||||||
titleDisplayMode() {
|
titleDisplayMode() {
|
||||||
@@ -479,11 +512,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Modal handling methods
|
// Modal handling methods
|
||||||
openPrivacyModal() {
|
openDpaModal() {
|
||||||
|
console.log('[DynamicForm] openDpaModal called');
|
||||||
this.loadContent('dpa');
|
this.loadContent('dpa');
|
||||||
},
|
},
|
||||||
|
|
||||||
openTermsModal() {
|
openTermsModal() {
|
||||||
|
console.log('[DynamicForm] openTermsModal called');
|
||||||
this.loadContent('terms');
|
this.loadContent('terms');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -505,6 +540,8 @@ export default {
|
|||||||
const title = contentType === 'dpa' ? 'Data Privacy Agreement' : 'Terms & Conditions';
|
const title = contentType === 'dpa' ? 'Data Privacy Agreement' : 'Terms & Conditions';
|
||||||
const contentUrl = `${this.apiPrefix}/${contentType}`;
|
const contentUrl = `${this.apiPrefix}/${contentType}`;
|
||||||
|
|
||||||
|
console.log('[DynamicForm] Loading content from:', contentUrl);
|
||||||
|
|
||||||
// Use the composable to show modal and load content
|
// Use the composable to show modal and load content
|
||||||
await this.contentModal.showModal({
|
await this.contentModal.showModal({
|
||||||
title: title,
|
title: title,
|
||||||
@@ -514,11 +551,19 @@ export default {
|
|||||||
|
|
||||||
// Handle Enter key press in form fields
|
// Handle Enter key press in form fields
|
||||||
handleEnterKey(event) {
|
handleEnterKey(event) {
|
||||||
console.log('DynamicForm: Enter event received, emitting form-enter-pressed');
|
console.log('DynamicForm: Enter event received');
|
||||||
// Prevent default form submission
|
// Prevent default form submission
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Emit event to parent (ChatInput) to trigger send
|
|
||||||
this.$emit('form-enter-pressed');
|
// Alleen submit toelaten als het formulier (inclusief consentvelden)
|
||||||
|
// geldig is. Hiermee worden keyboard-shortcuts uitgeschakeld zolang
|
||||||
|
// consent niet is gegeven of andere vereiste velden ontbreken.
|
||||||
|
if (this.isFormValid && !this.isSubmittingForm && !this.isSubmitting) {
|
||||||
|
// Emit event to parent (ChatInput) to trigger send
|
||||||
|
this.$emit('form-enter-pressed');
|
||||||
|
} else {
|
||||||
|
console.log('DynamicForm: Enter ignored because form is not valid or is submitting');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Focus management - auto-focus on first form field
|
// Focus management - auto-focus on first form field
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
:template="texts.consentRich"
|
:template="texts.consentRich"
|
||||||
:aria-privacy="texts.ariaPrivacy || 'Open dpa statement in a dialog'"
|
:aria-privacy="texts.ariaPrivacy || 'Open dpa statement in a dialog'"
|
||||||
:aria-terms="texts.ariaTerms || 'Open terms and conditions in a dialog'"
|
:aria-terms="texts.ariaTerms || 'Open terms and conditions in a dialog'"
|
||||||
@open-privacy="openPrivacyModal"
|
@open-dpa="openDpaModal"
|
||||||
@open-terms="openTermsModal"
|
@open-terms="openTermsModal"
|
||||||
/>
|
/>
|
||||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||||
@@ -234,7 +234,7 @@ export default {
|
|||||||
texts() {
|
texts() {
|
||||||
// Validate that consentRich exists and includes both required tags; otherwise fallback to English base
|
// Validate that consentRich exists and includes both required tags; otherwise fallback to English base
|
||||||
const hasValidRich = (t) => t && typeof t.consentRich === 'string'
|
const hasValidRich = (t) => t && typeof t.consentRich === 'string'
|
||||||
&& /<privacy>[\s\S]*?<\/privacy>/.test(t.consentRich)
|
&& /<dpa>[\s\S]*?<\/dpa>/.test(t.consentRich)
|
||||||
&& /<terms>[\s\S]*?<\/terms>/.test(t.consentRich);
|
&& /<terms>[\s\S]*?<\/terms>/.test(t.consentRich);
|
||||||
|
|
||||||
// 1) Prefer backend-provided rich string on the field's meta (already localized)
|
// 1) Prefer backend-provided rich string on the field's meta (already localized)
|
||||||
@@ -331,10 +331,12 @@ export default {
|
|||||||
this.value = file;
|
this.value = file;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openPrivacyModal() {
|
openDpaModal() {
|
||||||
|
console.log('[FormField] openDpaModal emitting open-dpa-modal');
|
||||||
this.$emit('open-dpa-modal');
|
this.$emit('open-dpa-modal');
|
||||||
},
|
},
|
||||||
openTermsModal() {
|
openTermsModal() {
|
||||||
|
console.log('[FormField] openTermsModal emitting open-terms-modal');
|
||||||
this.$emit('open-terms-modal');
|
this.$emit('open-terms-modal');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
:api-prefix="apiPrefix"
|
:api-prefix="apiPrefix"
|
||||||
:is-latest-ai-message="isLatestAiMessage(message)"
|
:is-latest-ai-message="isLatestAiMessage(message)"
|
||||||
@image-loaded="handleImageLoaded"
|
@image-loaded="handleImageLoaded"
|
||||||
@specialist-complete="$emit('specialist-complete', $event)"
|
@specialist-complete="handleSpecialistCompleteFromMessage"
|
||||||
@specialist-error="$emit('specialist-error', $event)"
|
@specialist-error="handleSpecialistErrorFromMessage"
|
||||||
></chat-message>
|
></chat-message>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -173,6 +173,15 @@ export default {
|
|||||||
if (this._resizeObserver) this._resizeObserver.disconnect();
|
if (this._resizeObserver) this._resizeObserver.disconnect();
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
async handleLanguageChange(event) {
|
||||||
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
||||||
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
|
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>
|
||||||
@@ -5,22 +5,11 @@
|
|||||||
:make-name="tenantMake.name"
|
:make-name="tenantMake.name"
|
||||||
class="mobile-logo"
|
class="mobile-logo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LanguageSelector
|
|
||||||
:initial-language="initialLanguage"
|
|
||||||
:current-language="currentLanguage"
|
|
||||||
:supported-language-details="supportedLanguageDetails"
|
|
||||||
:allowed-languages="allowedLanguages"
|
|
||||||
@language-changed="handleLanguageChange"
|
|
||||||
class="mobile-language-selector"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
|
||||||
import SideBarLogo from './SideBarLogo.vue';
|
import SideBarLogo from './SideBarLogo.vue';
|
||||||
import LanguageSelector from './LanguageSelector.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tenantMake: {
|
tenantMake: {
|
||||||
@@ -49,45 +38,21 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['language-changed']);
|
// Mobile header toont enkel het logo; taalkeuze gebeurt via de Setup-tab.
|
||||||
|
|
||||||
const currentLanguage = ref(props.initialLanguage);
|
|
||||||
|
|
||||||
const handleLanguageChange = (newLanguage) => {
|
|
||||||
currentLanguage.value = newLanguage;
|
|
||||||
|
|
||||||
// Emit to parent
|
|
||||||
emit('language-changed', newLanguage);
|
|
||||||
|
|
||||||
// Global event for backward compatibility
|
|
||||||
const globalEvent = new CustomEvent('language-changed', {
|
|
||||||
detail: { language: newLanguage }
|
|
||||||
});
|
|
||||||
document.dispatchEvent(globalEvent);
|
|
||||||
|
|
||||||
// Update chatConfig
|
|
||||||
if (window.chatConfig) {
|
|
||||||
window.chatConfig.language = newLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save preference
|
|
||||||
localStorage.setItem('preferredLanguage', newLanguage);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mobile-header {
|
.mobile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap; /* allow wrapping to next line on narrow screens */
|
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
background: var(--sidebar-background);
|
background: var(--sidebar-background);
|
||||||
color: var(--sidebar-color);
|
color: var(--sidebar-color);
|
||||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
max-width: 100%; /* never exceed viewport width */
|
max-width: 100%;
|
||||||
overflow: hidden; /* clip any accidental overflow */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile logo container - meer specifieke styling */
|
/* Mobile logo container - meer specifieke styling */
|
||||||
@@ -129,34 +94,6 @@ const handleLanguageChange = (newLanguage) => {
|
|||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile language selector styling */
|
|
||||||
.mobile-language-selector {
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 0; /* allow selector area to shrink */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-language-selector :deep(.language-selector) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-language-selector :deep(label) {
|
|
||||||
display: none; /* Hide label in mobile header */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-language-selector :deep(.language-select) {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
min-width: 0; /* allow the select to shrink */
|
|
||||||
max-width: 100%; /* never exceed container width */
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra constraints on ultra-small screens */
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.mobile-language-selector :deep(.language-select) {
|
|
||||||
max-width: 60vw; /* avoid pushing beyond viewport */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Media queries voor responsiviteit */
|
/* Media queries voor responsiviteit */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
113
eveai_chat_client/static/assets/vue-components/MobileTabBar.vue
Normal file
113
eveai_chat_client/static/assets/vue-components/MobileTabBar.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="mobile-tab-bar"
|
||||||
|
:class="{ 'mobile-tab-bar--header': placement === 'header' }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
type="button"
|
||||||
|
class="tab-button"
|
||||||
|
:class="{ 'is-active': tab.id === modelValue }"
|
||||||
|
@click="$emit('update:modelValue', tab.id)"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined" :class="`icon-${tab.iconName}`">
|
||||||
|
{{ tab.iconName }}
|
||||||
|
</span>
|
||||||
|
<span v-if="showLabels" class="tab-label">{{ tab.label }}</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
|
import { useIconManager } from '../js/composables/useIconManager.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'bottom' // 'bottom' | 'header'
|
||||||
|
},
|
||||||
|
showLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const { loadIcons } = useIconManager();
|
||||||
|
|
||||||
|
const iconNames = computed(() => props.tabs.map(t => t.iconName).filter(Boolean));
|
||||||
|
|
||||||
|
watch(iconNames, (names) => {
|
||||||
|
if (names && names.length) {
|
||||||
|
loadIcons(names);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 6px 8px;
|
||||||
|
padding-bottom: calc(6px + var(--safe-bottom-inset, 0px));
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: var(--tab-background, #0a0a0a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tab-bar--header {
|
||||||
|
/* In de header geen extra safe-area padding en geen border-top */
|
||||||
|
padding: 4px 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--tab-icon-inactive-color, rgba(240, 240, 240, 0.7));
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button .material-symbols-outlined {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.is-active {
|
||||||
|
color: var(--tab-icon-active-color, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.is-active .material-symbols-outlined {
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 1,
|
||||||
|
'wght' 500,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-tab-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,5 +24,8 @@ useChatViewport();
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sidebar-mobile-setup">
|
||||||
|
<SideBarMakeName
|
||||||
|
:make-name="tenantMake.name"
|
||||||
|
:subtitle="tenantMake.subtitle"
|
||||||
|
class="setup-make-name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LanguageSelector
|
||||||
|
:initial-language="initialLanguage"
|
||||||
|
:current-language="currentLanguageInternal"
|
||||||
|
:supported-language-details="supportedLanguageDetails"
|
||||||
|
:allowed-languages="allowedLanguages"
|
||||||
|
@language-changed="handleLanguageChange"
|
||||||
|
class="setup-language-selector"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SideBarExplanation
|
||||||
|
:original-text="explanationText"
|
||||||
|
:current-language="currentLanguageInternal"
|
||||||
|
:api-prefix="apiPrefix"
|
||||||
|
class="setup-explanation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import SideBarMakeName from './SideBarMakeName.vue';
|
||||||
|
import LanguageSelector from './LanguageSelector.vue';
|
||||||
|
import SideBarExplanation from './SideBarExplanation.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tenantMake: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
name: '',
|
||||||
|
subtitle: ''
|
||||||
|
})
|
||||||
|
},
|
||||||
|
explanationText: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
initialLanguage: {
|
||||||
|
type: String,
|
||||||
|
default: 'en'
|
||||||
|
},
|
||||||
|
currentLanguage: {
|
||||||
|
type: String,
|
||||||
|
default: 'en'
|
||||||
|
},
|
||||||
|
supportedLanguageDetails: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
allowedLanguages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['nl', 'en', 'fr', 'de']
|
||||||
|
},
|
||||||
|
apiPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['language-changed']);
|
||||||
|
|
||||||
|
const currentLanguageInternal = ref(props.currentLanguage || props.initialLanguage);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.currentLanguage,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && newVal !== currentLanguageInternal.value) {
|
||||||
|
currentLanguageInternal.value = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLanguageChange = (newLanguage) => {
|
||||||
|
currentLanguageInternal.value = newLanguage;
|
||||||
|
emit('language-changed', newLanguage);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-mobile-setup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 8px 16px 8px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-make-name {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-language-selector {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--sidebar-background, rgba(255, 255, 255, 0.02));
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-explanation {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.sidebar-mobile-setup {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -44,6 +44,11 @@
|
|||||||
--human-message-background: {{ customisation.human_message_background|default('#ffffff') }};
|
--human-message-background: {{ customisation.human_message_background|default('#ffffff') }};
|
||||||
--human-message-text-color: {{ customisation.human_message_text_color|default('#212529') }};
|
--human-message-text-color: {{ customisation.human_message_text_color|default('#212529') }};
|
||||||
|
|
||||||
|
/* Mobe Tab Bar Colors */
|
||||||
|
--tab-background: {{ customisation.tab_background|default('#0a0a0a') }};
|
||||||
|
--tab-icon-active-color: {{ customisation.tab_icon_active_color|default('#ffffff') }};
|
||||||
|
--tab-icon-inactive-color: {{ customisation.tab_icon_inactive_color|default('#f0f0f0') }};
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ def translate():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@chat_bp.route('/privacy', methods=['GET'])
|
@chat_bp.route('/dpa', methods=['GET'])
|
||||||
def privacy_statement():
|
def privacy_statement():
|
||||||
"""
|
"""
|
||||||
Public AJAX endpoint for dpa statement content
|
Public AJAX endpoint for dpa statement content
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ console.log('Components loaded:', Object.keys(Components));
|
|||||||
|
|
||||||
// Import specifieke componenten
|
// Import specifieke componenten
|
||||||
import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue';
|
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 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';
|
import SideBar from '../../../eveai_chat_client/static/assets/vue-components/SideBar.vue';
|
||||||
import MobileHeader from '../../../eveai_chat_client/static/assets/vue-components/MobileHeader.vue';
|
|
||||||
|
|
||||||
// VueUse-setup voor de chatclient (maakt composables beschikbaar via window.VueUse)
|
// VueUse-setup voor de chatclient (maakt composables beschikbaar via window.VueUse)
|
||||||
import './vueuse-setup.js';
|
import './vueuse-setup.js';
|
||||||
@@ -51,9 +52,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Initialiseer sidebar (vervangt fillSidebarExplanation en initializeLanguageSelector)
|
// Initialiseer sidebar (vervangt fillSidebarExplanation en initializeLanguageSelector)
|
||||||
initializeSidebar();
|
initializeSidebar();
|
||||||
|
|
||||||
// Initialiseer mobile header
|
|
||||||
initializeMobileHeader();
|
|
||||||
|
|
||||||
// Initialiseer chat app (simpel)
|
// Initialiseer chat app (simpel)
|
||||||
initializeChatApp();
|
initializeChatApp();
|
||||||
});
|
});
|
||||||
@@ -121,85 +119,8 @@ function initializeSidebar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// initializeMobileHeader is verwijderd; de mobiele header wordt nu volledig
|
||||||
* Initialiseert de mobile header component
|
// binnen ChatApp.vue beheerd.
|
||||||
*/
|
|
||||||
function initializeMobileHeader() {
|
|
||||||
const container = document.getElementById('mobile-header-container');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('#mobile-header-container niet gevonden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Maak props voor de component
|
|
||||||
const props = {
|
|
||||||
tenantMake: {
|
|
||||||
name: window.chatConfig.tenantMake?.name || '',
|
|
||||||
logo_url: window.chatConfig.tenantMake?.logo_url || '',
|
|
||||||
subtitle: window.chatConfig.tenantMake?.subtitle || ''
|
|
||||||
},
|
|
||||||
initialLanguage: window.chatConfig.language || 'nl',
|
|
||||||
supportedLanguageDetails: window.chatConfig.supportedLanguageDetails || {},
|
|
||||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
|
||||||
apiPrefix: window.chatConfig.apiPrefix || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mount de component
|
|
||||||
const app = createApp(MobileHeader, props);
|
|
||||||
|
|
||||||
// Create and provide LanguageProvider for mobile header components
|
|
||||||
const initialLanguage = window.chatConfig?.language || 'nl';
|
|
||||||
const apiPrefix = window.chatConfig?.apiPrefix || '';
|
|
||||||
const languageProvider = createLanguageProvider(initialLanguage, apiPrefix);
|
|
||||||
app.provide(LANGUAGE_PROVIDER_KEY, languageProvider);
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
app.config.errorHandler = (err, vm, info) => {
|
|
||||||
console.error('🚨 [Vue Error in MobileHeader]', err);
|
|
||||||
console.error('Component:', vm);
|
|
||||||
console.error('Error Info:', info);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mountedApp = app.mount(container);
|
|
||||||
|
|
||||||
// Dynamisch de headerhoogte doorgeven aan CSS
|
|
||||||
const updateHeaderHeightVar = () => {
|
|
||||||
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
||||||
if (!isMobile) {
|
|
||||||
document.documentElement.style.removeProperty('--mobile-header-height');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const h = container.offsetHeight || 60; // fallback
|
|
||||||
document.documentElement.style.setProperty('--mobile-header-height', `${h}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initieel instellen en bij gebeurtenissen herberekenen
|
|
||||||
requestAnimationFrame(updateHeaderHeightVar);
|
|
||||||
window.addEventListener('resize', updateHeaderHeightVar);
|
|
||||||
|
|
||||||
// Listen to language change events and update the mobile header's language provider
|
|
||||||
const languageChangeHandler = (event) => {
|
|
||||||
if (event.detail && event.detail.language) {
|
|
||||||
console.log('MobileHeader: Received language change event:', event.detail.language);
|
|
||||||
languageProvider.setLanguage(event.detail.language);
|
|
||||||
// taalwissel kan headerhoogte veranderen
|
|
||||||
requestAnimationFrame(updateHeaderHeightVar);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('language-changed', languageChangeHandler);
|
|
||||||
|
|
||||||
// Store the handler for cleanup if needed
|
|
||||||
mountedApp._languageChangeHandler = languageChangeHandler;
|
|
||||||
mountedApp._updateHeaderHeightVar = updateHeaderHeightVar;
|
|
||||||
|
|
||||||
console.log('✅ MobileHeader component successfully mounted with LanguageProvider en dynamische headerhoogte');
|
|
||||||
return mountedApp;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('🚨 [CRITICAL ERROR] Bij initialiseren mobile header:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialiseert de chat app (Vue component)
|
* Initialiseert de chat app (Vue component)
|
||||||
@@ -212,27 +133,43 @@ function initializeChatApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ChatApp) {
|
|
||||||
throw new Error('🚨 [CRITICAL ERROR] ChatApp component niet gevonden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra verificatie dat alle sub-componenten beschikbaar zijn
|
// Extra verificatie dat alle sub-componenten beschikbaar zijn
|
||||||
if (!Components.MessageHistory || !Components.ChatInput ||
|
if (!Components.MessageHistory || !Components.ChatInput ||
|
||||||
!Components.TypingIndicator || !Components.ChatMessage) {
|
!Components.TypingIndicator || !Components.ChatMessage) {
|
||||||
console.warn('⚠️ [WARN] Niet alle benodigde sub-componenten zijn geladen!');
|
console.warn('⚠️ [WARN] Niet alle benodigde sub-componenten zijn geladen!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maak props voor de component
|
// Maak props voor de shells / CoreChatApp
|
||||||
const props = {
|
const baseProps = {
|
||||||
apiPrefix: window.chatConfig.apiPrefix || '',
|
apiPrefix: window.chatConfig.apiPrefix || '',
|
||||||
conversationId: window.chatConfig.conversationId || 'default',
|
conversationId: window.chatConfig.conversationId || 'default',
|
||||||
userId: window.chatConfig.userId || null,
|
userId: window.chatConfig.userId || null,
|
||||||
userName: window.chatConfig.userName || '',
|
userName: window.chatConfig.userName || '',
|
||||||
initialLanguage: window.chatConfig.language || 'nl',
|
initialLanguage: window.chatConfig.language || 'nl',
|
||||||
supportedLanguageDetails: window.chatConfig.supportedLanguageDetails || {},
|
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
|
// Mount de component via ChatRoot zodat SafeViewport de layout kan beheren
|
||||||
const app = createApp(ChatRoot, props);
|
const app = createApp(ChatRoot, props);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user