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 @@
+
+