Merge branch 'feature/Introduce_tabs_in_mobile_chat_client' into develop
This commit is contained in:
@@ -36,7 +36,10 @@ def get_default_chat_customisation(tenant_customisation=None):
|
||||
'ai_message_text_color': '#212529',
|
||||
'human_message_background': '#212529',
|
||||
'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
|
||||
|
||||
@@ -87,6 +87,21 @@ configuration:
|
||||
description: "Human Message Inactive Text Color"
|
||||
type: "color"
|
||||
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:
|
||||
author: "Josako"
|
||||
date_added: "2024-06-06"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dist/chat-client.js": "dist/chat-client.f8ee4d5a.js",
|
||||
"dist/chat-client.css": "dist/chat-client.2fffefae.css",
|
||||
"dist/chat-client.js": "dist/chat-client.5b709f8c.js",
|
||||
"dist/chat-client.css": "dist/chat-client.cb306abb.css",
|
||||
"dist/main.js": "dist/main.6a617099.js",
|
||||
"dist/main.css": "dist/main.7182aac3.css"
|
||||
}
|
||||
@@ -21,7 +21,9 @@
|
||||
/* App container layout */
|
||||
.app-container {
|
||||
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;
|
||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||
width: 100%;
|
||||
@@ -93,7 +95,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: auto; /* prefer dynamic viewport on desktop */
|
||||
height: auto; /* desktop: dynamische hoogte, op mobiel overschreven */
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
@@ -103,6 +105,26 @@
|
||||
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 {
|
||||
height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100);
|
||||
min-height: 0;
|
||||
|
||||
@@ -1,38 +1,106 @@
|
||||
active_text_color<template>
|
||||
<div class="chat-app-container">
|
||||
<!-- Message History - takes available space -->
|
||||
<message-history
|
||||
:messages="displayMessages"
|
||||
:is-typing="isTyping"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:auto-scroll="true"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
></message-history>
|
||||
<!-- Desktop layout: huidige gedrag behouden -->
|
||||
<div v-if="!isMobileFallback" class="chat-desktop-layout">
|
||||
<message-history
|
||||
:messages="displayMessages"
|
||||
:is-typing="isTyping"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:auto-scroll="true"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
></message-history>
|
||||
|
||||
<!-- Chat Input - to the bottom -->
|
||||
<chat-input
|
||||
:current-message="currentMessage"
|
||||
:is-loading="isLoading"
|
||||
:max-length="2000"
|
||||
:allow-file-upload="true"
|
||||
:allow-voice-message="false"
|
||||
:form-data="currentInputFormData"
|
||||
:active-ai-message="activeAiMessage"
|
||||
:api-prefix="apiPrefix"
|
||||
@send-message="sendMessage"
|
||||
@update-message="updateCurrentMessage"
|
||||
@upload-file="handleFileUpload"
|
||||
@record-voice="handleVoiceRecord"
|
||||
@submit-form="submitFormFromInput"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="chatInput"
|
||||
class="chat-input-area"
|
||||
></chat-input>
|
||||
<chat-input
|
||||
:current-message="currentMessage"
|
||||
:is-loading="isLoading"
|
||||
:max-length="2000"
|
||||
:allow-file-upload="true"
|
||||
:allow-voice-message="false"
|
||||
:form-data="currentInputFormData"
|
||||
:active-ai-message="activeAiMessage"
|
||||
:api-prefix="apiPrefix"
|
||||
@send-message="sendMessage"
|
||||
@update-message="updateCurrentMessage"
|
||||
@upload-file="handleFileUpload"
|
||||
@record-voice="handleVoiceRecord"
|
||||
@submit-form="submitFormFromInput"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="chatInput"
|
||||
class="chat-input-area"
|
||||
></chat-input>
|
||||
</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
|
||||
@@ -60,6 +128,9 @@ import ProgressTracker from './ProgressTracker.vue';
|
||||
import LanguageSelector from './LanguageSelector.vue';
|
||||
import ChatInput from './ChatInput.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 { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
|
||||
@@ -77,7 +148,10 @@ export default {
|
||||
MessageHistory,
|
||||
ProgressTracker,
|
||||
ChatInput,
|
||||
ContentModal
|
||||
ContentModal,
|
||||
SideBarLogo,
|
||||
MobileTabBar,
|
||||
SideBarMobileSetup
|
||||
},
|
||||
|
||||
setup() {
|
||||
@@ -90,7 +164,7 @@ export default {
|
||||
|
||||
// Creëer en provide content modal
|
||||
const contentModal = provideContentModal();
|
||||
|
||||
|
||||
// Provide aan alle child components
|
||||
provide(LANGUAGE_PROVIDER_KEY, languageProvider);
|
||||
|
||||
@@ -111,6 +185,7 @@ export default {
|
||||
return {
|
||||
// Tenant info
|
||||
tenantName: tenantMake.name || 'EveAI',
|
||||
tenantSubtitle: tenantMake.subtitle || '',
|
||||
tenantLogoUrl: tenantMake.logo_url || '',
|
||||
|
||||
// Taal gerelateerde data
|
||||
@@ -147,10 +222,13 @@ export default {
|
||||
autoScroll: settings.autoScroll === true
|
||||
},
|
||||
|
||||
// UI state
|
||||
isMobile: window.innerWidth <= 768,
|
||||
// UI state (fallback flags voor oudere logica)
|
||||
isMobileFallback: window.innerWidth <= 768,
|
||||
showSidebar: window.innerWidth > 768,
|
||||
|
||||
// Mobile tab state
|
||||
activeTabId: 'chat',
|
||||
|
||||
// Advanced features
|
||||
messageSearch: '',
|
||||
filteredMessages: [],
|
||||
@@ -193,16 +271,57 @@ export default {
|
||||
return this.supportedLanguages.filter(lang =>
|
||||
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() {
|
||||
this.initializeChat();
|
||||
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() {
|
||||
this.cleanup();
|
||||
|
||||
if (this.globalTabListener) {
|
||||
document.removeEventListener('evie-chat-set-tab', this.globalTabListener);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -453,11 +572,13 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
// Window resize listener
|
||||
window.addEventListener('resize', () => {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
// Window resize listener voor fallback flags
|
||||
this.handleResize = () => {
|
||||
this.isMobileFallback = window.innerWidth <= 768;
|
||||
this.showSidebar = window.innerWidth > 768;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
@@ -516,6 +637,12 @@ export default {
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
handleLanguageChangedFromSetup(newLanguage) {
|
||||
// Update lokale taalstate; verdere effecten worden opgepikt door
|
||||
// bestaande global listener en LanguageProvider / chatConfig.
|
||||
this.currentLanguage = newLanguage;
|
||||
},
|
||||
|
||||
// UI helpers
|
||||
scrollToBottom() {
|
||||
if (this.$refs.messageHistory) {
|
||||
@@ -560,7 +687,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
/* height: 100%; avoided to let flex sizing control height */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
max-width: 1000px;
|
||||
@@ -571,6 +698,84 @@ export default {
|
||||
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 {
|
||||
flex: 1;
|
||||
min-height: 0; /* ensure child can scroll */
|
||||
|
||||
@@ -452,6 +452,7 @@ export default {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
max-height: 33vh; /* fallback */
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
||||
-webkit-overflow-scrolling: touch; /* iOS smooth inertia */
|
||||
max-height: 33vh; /* fallback */
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* prevent scroll chaining to parent */
|
||||
-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.sticky-area .message-content {
|
||||
max-height: 50vh; /* fallback */
|
||||
max-height: 50vh; /* fallback */
|
||||
}
|
||||
|
||||
@supports (max-height: 1svh) {
|
||||
.message .message-content { max-height: 33svh; }
|
||||
.message.input-area .message-content,
|
||||
.message.sticky-area .message-content { max-height: 50svh; }
|
||||
.message .message-content {
|
||||
/* Gebruik veilige viewporthoogte die door useChatViewport gezet wordt */
|
||||
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>
|
||||
@@ -5,22 +5,11 @@
|
||||
:make-name="tenantMake.name"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import SideBarLogo from './SideBarLogo.vue';
|
||||
import LanguageSelector from './LanguageSelector.vue';
|
||||
|
||||
const props = defineProps({
|
||||
tenantMake: {
|
||||
@@ -49,45 +38,21 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['language-changed']);
|
||||
|
||||
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);
|
||||
};
|
||||
// Mobile header toont enkel het logo; taalkeuze gebeurt via de Setup-tab.
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap; /* allow wrapping to next line on narrow screens */
|
||||
padding: 10px 15px;
|
||||
background: var(--sidebar-background);
|
||||
color: var(--sidebar-color);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
min-height: 60px;
|
||||
max-width: 100%; /* never exceed viewport width */
|
||||
overflow: hidden; /* clip any accidental overflow */
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Mobile logo container - meer specifieke styling */
|
||||
@@ -129,34 +94,6 @@ const handleLanguageChange = (newLanguage) => {
|
||||
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 (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%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</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-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>
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-compo
|
||||
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 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)
|
||||
import './vueuse-setup.js';
|
||||
@@ -51,9 +50,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialiseer sidebar (vervangt fillSidebarExplanation en initializeLanguageSelector)
|
||||
initializeSidebar();
|
||||
|
||||
// Initialiseer mobile header
|
||||
initializeMobileHeader();
|
||||
|
||||
// Initialiseer chat app (simpel)
|
||||
initializeChatApp();
|
||||
});
|
||||
@@ -121,85 +117,8 @@ function initializeSidebar() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialiseert de mobile header component
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
// initializeMobileHeader is verwijderd; de mobiele header wordt nu volledig
|
||||
// binnen ChatApp.vue beheerd.
|
||||
|
||||
/**
|
||||
* Initialiseert de chat app (Vue component)
|
||||
|
||||
Reference in New Issue
Block a user