- Full implementation of tab bar next to logo in mobile client

- Customisation option in Tenant Make
- Splitting all controls in the newly created tabs
This commit is contained in:
Josako
2025-11-27 11:32:46 +01:00
parent d68dfde52a
commit 14273b8a70
11 changed files with 524 additions and 205 deletions

View File

@@ -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="!isMobile" 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,12 +128,16 @@ 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';
// Import content modal composable
import { provideContentModal } from '../js/composables/useContentModal.js';
import { provide } from 'vue';
import useChatViewport from '../js/composables/useChatViewport.js';
export default {
name: 'ChatApp',
@@ -77,7 +149,10 @@ export default {
MessageHistory,
ProgressTracker,
ChatInput,
ContentModal
ContentModal,
SideBarLogo,
MobileTabBar,
SideBarMobileSetup
},
setup() {
@@ -90,13 +165,17 @@ export default {
// Creëer en provide content modal
const contentModal = provideContentModal();
// Initialiseer viewport-/device-informatie via useChatViewport (VueUse)
const { isMobile } = useChatViewport();
// Provide aan alle child components
provide(LANGUAGE_PROVIDER_KEY, languageProvider);
return {
languageProvider,
contentModal
contentModal,
isMobile
};
},
@@ -111,6 +190,7 @@ export default {
return {
// Tenant info
tenantName: tenantMake.name || 'EveAI',
tenantSubtitle: tenantMake.subtitle || '',
tenantLogoUrl: tenantMake.logo_url || '',
// Taal gerelateerde data
@@ -147,10 +227,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 +276,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 +577,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 +642,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 +692,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 +703,71 @@ export default {
overflow: hidden;
}
.chat-mobile-layout {
display: flex;
flex-direction: column;
height: 100%;
min-height: calc(var(--safe-vh, 1vh) * 100);
}
.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));
}
@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 */