diff --git a/config/static-manifest/manifest.json b/config/static-manifest/manifest.json index 0bb1b16..f2145d6 100644 --- a/config/static-manifest/manifest.json +++ b/config/static-manifest/manifest.json @@ -1,6 +1,6 @@ { - "dist/chat-client.js": "dist/chat-client.f7134231.js", - "dist/chat-client.css": "dist/chat-client.99e10656.css", + "dist/chat-client.js": "dist/chat-client.f8ee4d5a.js", + "dist/chat-client.css": "dist/chat-client.2fffefae.css", "dist/main.js": "dist/main.6a617099.js", "dist/main.css": "dist/main.7182aac3.css" } \ No newline at end of file diff --git a/content/changelog/1.0/1.0.0.md b/content/changelog/1.0/1.0.0.md index 5d45f2e..bacfe0b 100644 --- a/content/changelog/1.0/1.0.0.md +++ b/content/changelog/1.0/1.0.0.md @@ -5,6 +5,17 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.1.26-beta + +Release date: + +### Changed +- Introduction of vueuse/core in the chat client, to ensure abstraction of ui behaviour for different mobile devices. + +### Fixed +- TRA-99, fixed creation of a new Tenant Make. +- Improvement of DynamicFormBase to better align behaviour with the standard FlaskForm. + ## 3.1.24-beta Release date: 2025-11-25 diff --git a/eveai_app/templates/user/tenant_make.html b/eveai_app/templates/user/tenant_make.html index b5c1418..6620f8f 100644 --- a/eveai_app/templates/user/tenant_make.html +++ b/eveai_app/templates/user/tenant_make.html @@ -11,18 +11,9 @@ {{ form.hidden_tag() }} {% set disabled_fields = [] %} {% set exclude_fields = [] %} - {% for field in form.get_static_fields() %} + {% for field in form %} {{ render_field(field, disabled_fields, exclude_fields) }} {% endfor %} - - {% for collection_name, fields in form.get_dynamic_fields().items() %} - {% if fields|length > 0 %} -

{{ collection_name }}

- {% endif %} - {% for field in fields %} - {{ render_field(field, disabled_fields, exclude_fields) }} - {% endfor %} - {% endfor %} {% endblock %} diff --git a/eveai_app/views/dynamic_form_base.py b/eveai_app/views/dynamic_form_base.py index 5d4fbbf..abe8d89 100644 --- a/eveai_app/views/dynamic_form_base.py +++ b/eveai_app/views/dynamic_form_base.py @@ -96,14 +96,42 @@ class OrderedListField(TextAreaField): class DynamicFormBase(FlaskForm): - def __init__(self, formdata=None, *args, **kwargs): - # Belangrijk: formdata doorgeven aan FlaskForm zodat WTForms POST-data kan binden - super(DynamicFormBase, self).__init__(formdata=formdata, *args, **kwargs) + def __init__(self, *args, **kwargs): + """Base class voor dynamische formulieren. + + Belangrijk ontwerpprincipe: + - We laten "normaal" FlaskForm-gedrag intact. Dat betekent dat als een view + geen expliciete ``formdata=`` meegeeft, FlaskForm zelf beslist of en hoe + ``request.form`` wordt gebruikt (inclusief CSRF-handling). + - Als een view wel expliciet ``formdata=`` meegeeft, wordt dat gewoon door de + superklasse afgehandeld. + + Hierdoor hoeven views DynamicFormBase niet anders te behandelen dan een + standaard FlaskForm; dynamische velden zijn een extra laag bovenop het + standaard gedrag. + """ + + # Laat FlaskForm alle standaard initialisatielogica uitvoeren + super(DynamicFormBase, self).__init__(*args, **kwargs) + # Maps collection names to lists of field names self.dynamic_fields = {} - # Store formdata for later use - self.formdata = formdata - self.raw_formdata = request.form.to_dict() + + # Bepaal effectieve formdata voor intern gebruik. + # In de meeste gevallen is dat bij POST gewoon request.form; bij GET is er + # doorgaans geen formdata en vertrouwen we op object-binding en defaults. + if request.method == 'POST': + self.formdata = request.form + # Bewaar een eenvoudige dict-weergave voor hulplogica zoals + # get_dynamic_data (bijvoorbeeld voor BooleanFields) + try: + self.raw_formdata = request.form.to_dict(flat=False) + except TypeError: + # Fallback voor oudere/afwijkende MultiDict-implementaties + self.raw_formdata = request.form.to_dict() + else: + self.formdata = None + self.raw_formdata = {} def _create_field_validators(self, field_def): """Create validators based on field definition""" diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py index f99916d..22b33f8 100644 --- a/eveai_app/views/user_forms.py +++ b/eveai_app/views/user_forms.py @@ -177,7 +177,7 @@ def validate_make_name(form, field): raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.') -class TenantMakeForm(DynamicFormBase): +class TenantMakeForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name]) description = TextAreaField('Description', validators=[Optional()]) active = BooleanField('Active', validators=[Optional()], default=True) diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 3e25fdd..458ee14 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -594,8 +594,6 @@ def delete_tenant_project(tenant_project_id): @roles_accepted('Super User', 'Partner Admin', 'Tenant Admin') def tenant_make(): form = TenantMakeForm() - customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") - default_customisation_options = create_default_config_from_type_config(customisation_config["configuration"]) if form.validate_on_submit(): tenant_id = session['tenant']['id'] @@ -612,7 +610,7 @@ def tenant_make(): flash('Tenant Make successfully added!', 'success') current_app.logger.info(f'Tenant Make {new_tenant_make.name}, id {new_tenant_make.id} successfully added ' f'for tenant {tenant_id}!') - # Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type) + # Enable step 2 of creation of make - add configuration of the retriever (dependent on type) return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=new_tenant_make.id, for_redirect=True)) except SQLAlchemyError as e: db.session.rollback() @@ -638,7 +636,9 @@ def edit_tenant_make(tenant_make_id): # Get the tenant make or return 404 tenant_make = TenantMake.query.get_or_404(tenant_make_id) - # Create form instance with the tenant make + # Create form instance with the tenant make. + # Dankzij DynamicFormBase wordt formdata nu op standaard FlaskForm-manier + # afgehandeld en is geen expliciete formdata=request.form meer nodig. form = EditTenantMakeForm(obj=tenant_make) # Initialiseer de allowed_languages selectie met huidige waarden diff --git a/eveai_chat_client/static/assets/css/chat.css b/eveai_chat_client/static/assets/css/chat.css index 11daad1..6bb3633 100644 --- a/eveai_chat_client/static/assets/css/chat.css +++ b/eveai_chat_client/static/assets/css/chat.css @@ -9,6 +9,13 @@ --message-bot-bg: #f8f9fa; --border-radius: 8px; --spacing: 16px; + + /* Nieuwe, veilige viewport-variabelen voor mobiele lay-out + - --vvh blijft als fallback uit viewport.js + - --safe-vh wordt gezet door useChatViewport() + - --safe-bottom-inset wordt gebruikt voor ondermarge bij de chat input */ + --safe-vh: var(--vvh, 1vh); + --safe-bottom-inset: 0px; } /* App container layout */ @@ -16,7 +23,7 @@ display: flex; /* Use visual viewport variable when available */ min-height: 0; - height: calc(var(--vvh, 1vh) * 100); + height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100); width: 100%; } @@ -97,7 +104,8 @@ } html, body { - height: calc(var(--vvh, 1vh) * 100); min-height: 0; + height: calc(var(--safe-vh, var(--vvh, 1vh)) * 100); + min-height: 0; } /* Base reset & overflow control */ diff --git a/eveai_chat_client/static/assets/js/composables/useChatViewport.js b/eveai_chat_client/static/assets/js/composables/useChatViewport.js new file mode 100644 index 0000000..1de2f8d --- /dev/null +++ b/eveai_chat_client/static/assets/js/composables/useChatViewport.js @@ -0,0 +1,176 @@ +// useChatViewport.js +// Centrale viewport-/keyboard-laag voor de chat client. +// - Gebruikt visualViewport + VueUse (via window.VueUse) om veilige hoogte +// en keyboard-status te bepalen +// - Stelt CSS-variabelen --safe-vh en --safe-bottom-inset in op +// - Beheert body-klassen zoals chat-keyboard-open en ios-safari + +import { ref, computed, watchEffect, onMounted, onBeforeUnmount } from 'vue'; + +// Haal VueUse-composables uit window.VueUse, die in de chat-bundel +// worden geïnitialiseerd via nginx/frontend_src/js/vueuse-setup.js. +// Als ze ontbreken (bv. in een testsituatie), vallen we terug op +// eenvoudige lokale shims. + +const vueUse = (typeof window !== 'undefined' && window.VueUse) || {}; +let { useEventListener, useWindowSize } = vueUse; + +// Fallback-shim voor useEventListener +if (!useEventListener) { + useEventListener = (target, event, handler, options) => { + if (!target || !target.addEventListener) return () => {}; + const opts = options || { passive: true }; + target.addEventListener(event, handler, opts); + return () => target.removeEventListener(event, handler, opts); + }; +} + +// Fallback-shim voor useWindowSize +if (!useWindowSize) { + useWindowSize = () => { + const height = ref(typeof window !== 'undefined' ? window.innerHeight : 0); + if (typeof window !== 'undefined') { + window.addEventListener('resize', () => { + height.value = window.innerHeight; + }, { passive: true }); + } + return { height }; + }; +} + +function detectIosSafari() { + if (typeof navigator === 'undefined') return false; + const ua = navigator.userAgent || navigator.vendor || (window && window.opera) || ''; + const isIOS = /iP(ad|hone|od)/.test(ua); + const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS/.test(ua); + return isIOS && isSafari; +} + +// Drempel voor het detecteren van keyboard-open op basis van hoogteverschil +const KEYBOARD_DELTA_THRESHOLD = 120; // px, afgestemd op bestaande viewport.js heuristiek + +export function useChatViewport() { + const isClient = typeof window !== 'undefined' && typeof document !== 'undefined'; + + const safeHeight = ref(isClient ? window.innerHeight : 0); + const baselineHeight = ref(null); // baseline voor keyboard-detectie + const keyboardOpen = ref(false); + const safeBottomInset = ref(0); + + const { height: windowHeight } = useWindowSize(); + const isIosSafari = ref(isClient ? detectIosSafari() : false); + + const isMobile = computed(() => { + if (!isClient) return false; + // Houd de definitie in lijn met bestaande CSS (max-width: 768px) + return window.innerWidth <= 768; + }); + + function applyToCss() { + if (!isClient) return; + const root = document.documentElement; + const vhUnit = (safeHeight.value || windowHeight.value || 0) / 100; + + if (vhUnit > 0) { + root.style.setProperty('--safe-vh', `${vhUnit}px`); + } + + root.style.setProperty('--safe-bottom-inset', `${safeBottomInset.value || 0}px`); + + // Body-klassen + document.body.classList.toggle('chat-keyboard-open', !!keyboardOpen.value); + + if (isIosSafari.value) { + root.classList.add('ios-safari'); + } else { + root.classList.remove('ios-safari'); + } + } + + function updateFromVisualViewport() { + if (!isClient) return; + const vv = window.visualViewport; + + const currentHeight = vv ? vv.height : window.innerHeight; + safeHeight.value = currentHeight; + + // Bepaal een eenvoudige bottom-inset: verschil tussen layout- en visual viewport + const layoutHeight = window.innerHeight || currentHeight; + const inset = Math.max(0, layoutHeight - currentHeight); + safeBottomInset.value = inset; + + // Keyboard-detectie: vergelijk met baseline + if (baselineHeight.value == null) { + baselineHeight.value = currentHeight; + } + const delta = (baselineHeight.value || currentHeight) - currentHeight; + keyboardOpen.value = delta > KEYBOARD_DELTA_THRESHOLD; + } + + onMounted(() => { + if (!isClient) return; + + // Initiale meting + updateFromVisualViewport(); + applyToCss(); + + const vv = window.visualViewport; + + // Luister naar relevante events via VueUse + const stopResize = vv + ? useEventListener(vv, 'resize', () => { + updateFromVisualViewport(); + applyToCss(); + }) + : useEventListener(window, 'resize', () => { + updateFromVisualViewport(); + applyToCss(); + }); + + const stopScroll = vv + ? useEventListener(vv, 'scroll', () => { + updateFromVisualViewport(); + applyToCss(); + }) + : () => {}; + + const stopOrientation = useEventListener(window, 'orientationchange', () => { + // Bij oriëntatie-wijziging baseline resetten + baselineHeight.value = null; + updateFromVisualViewport(); + applyToCss(); + }); + + onBeforeUnmount(() => { + stopResize && stopResize(); + stopScroll && stopScroll(); + stopOrientation && stopOrientation(); + }); + }); + + // Reageer ook op generieke windowHeight veranderingen (fallback) + watchEffect(() => { + if (!isClient) return; + if (!window.visualViewport) { + safeHeight.value = windowHeight.value; + safeBottomInset.value = 0; + keyboardOpen.value = false; + applyToCss(); + } + }); + + // Initiale toepassing voor SSR / eerste render + if (isClient) { + applyToCss(); + } + + return { + safeHeight, + safeBottomInset, + keyboardOpen, + isMobile, + isIosSafari, + }; +} + +export default useChatViewport; diff --git a/eveai_chat_client/static/assets/vue-components/ChatRoot.vue b/eveai_chat_client/static/assets/vue-components/ChatRoot.vue new file mode 100644 index 0000000..ab3e903 --- /dev/null +++ b/eveai_chat_client/static/assets/vue-components/ChatRoot.vue @@ -0,0 +1,13 @@ + + + diff --git a/eveai_chat_client/static/assets/vue-components/SafeViewport.vue b/eveai_chat_client/static/assets/vue-components/SafeViewport.vue new file mode 100644 index 0000000..79d63c2 --- /dev/null +++ b/eveai_chat_client/static/assets/vue-components/SafeViewport.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/nginx/frontend_src/js/chat-client.js b/nginx/frontend_src/js/chat-client.js index 6917659..901911f 100644 --- a/nginx/frontend_src/js/chat-client.js +++ b/nginx/frontend_src/js/chat-client.js @@ -10,7 +10,6 @@ import '../../../eveai_chat_client/static/assets/css/form-message.css'; // Dependencies import { createApp, version } from 'vue'; import { marked } from 'marked'; -import { FormField } from '../../../../../../../../../Users/josako/Library/Application Support/JetBrains/PyCharm2025.1/scratches/old js files/FormField.js'; // Import LanguageProvider for sidebar translation support import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../../../eveai_chat_client/static/assets/js/services/LanguageProvider.js'; @@ -29,9 +28,13 @@ console.log('Components loaded:', Object.keys(Components)); // Import specifieke componenten import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue'; import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue'; +import 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'; + // Globale Vue error tracking window.addEventListener('error', function(event) { console.error('🚨 [Global Error]', event.error); @@ -230,8 +233,8 @@ function initializeChatApp() { allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'] }; - // Mount de component met alle nodige componenten - const app = createApp(ChatApp, props); + // Mount de component via ChatRoot zodat SafeViewport de layout kan beheren + const app = createApp(ChatRoot, props); // SSE verbinding configuratie - injecteren in ChatApp component app.provide('sseConfig', { diff --git a/nginx/frontend_src/js/vueuse-setup.js b/nginx/frontend_src/js/vueuse-setup.js new file mode 100644 index 0000000..f4cecc0 --- /dev/null +++ b/nginx/frontend_src/js/vueuse-setup.js @@ -0,0 +1,15 @@ +// vueuse-setup.js +// Setup-module voor VueUse in de chat client. +// - Importeert benodigde composables uit '@vueuse/core' +// - Maakt ze beschikbaar via window.VueUse zodat static assets +// onder eveai_chat_client/static ze kunnen gebruiken zonder +// directe npm-imports. + +import { useEventListener, useWindowSize } from '@vueuse/core'; + +if (typeof window !== 'undefined') { + window.VueUse = Object.assign({}, window.VueUse, { + useEventListener, + useWindowSize, + }); +} diff --git a/nginx/package-lock.json b/nginx/package-lock.json index ce48449..037034f 100644 --- a/nginx/package-lock.json +++ b/nginx/package-lock.json @@ -6,12 +6,13 @@ "": { "dependencies": { "@popperjs/core": "^2.11.8", + "@vueuse/core": "^14.0.0", "animejs": "^4.0.2", "bootstrap": "^5.3.6", "datatables.net": "^2.3.1", "highlight.js": "^11.11.1", "jquery": "^3.7.1", - "marked": "^16.0.0", + "marked": "16.3.0", "nouislider": "^15.8.1", "parallax": "^0.0.0", "prismjs": "^1.30.0", @@ -22,7 +23,6 @@ "vue": "^3.5.17" }, "devDependencies": { - "@parcel/reporter-bundle-analyzer": "^2.15.2", "@parcel/transformer-sass": "^2.15.2", "@parcel/transformer-vue": "^2.15.2", "parcel": "^2.15.2" @@ -1360,26 +1360,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/reporter-bundle-analyzer": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/reporter-bundle-analyzer/-/reporter-bundle-analyzer-2.16.0.tgz", - "integrity": "sha512-81EazkM7YjeFvzRRlKKV8kmLfLXfWlfLqlpC8jFRXthC43aZtdqMnzRgi9Q+6FIB7Ft3vnMXZPqRs4cn7+XgUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/plugin": "2.16.0", - "@parcel/utils": "2.16.0", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">= 16.0.0", - "parcel": "^2.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/reporter-cli": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.0.tgz", @@ -2722,6 +2702,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, "node_modules/@vue/compiler-core": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", @@ -2822,6 +2808,44 @@ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "license": "MIT" }, + "node_modules/@vueuse/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz", + "integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.0.0", + "@vueuse/shared": "14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz", + "integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0.tgz", + "integrity": "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", diff --git a/nginx/package.json b/nginx/package.json index dc12848..20f3e60 100644 --- a/nginx/package.json +++ b/nginx/package.json @@ -14,7 +14,8 @@ "tabulator-tables": "^6.3.1", "typed.js": "^2.1.0", "vanilla-jsoneditor": "^3.5.0", - "vue": "^3.5.17" + "vue": "^3.5.17", + "@vueuse/core": "^14.0.0" }, "devDependencies": { "@parcel/transformer-sass": "^2.15.2",