Merge branch 'release/v3.1.26-beta'

This commit is contained in:
Josako
2025-11-26 11:40:01 +01:00
14 changed files with 349 additions and 51 deletions

View File

@@ -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"
}

View File

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

View File

@@ -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 %}
<!-- Render Dynamic Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
<button type="submit" class="btn btn-primary">Register Tenant Make</button>
</form>
{% endblock %}

View File

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

View File

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

View File

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

View File

@@ -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 */

View File

@@ -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 <html>
// - 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;

View File

@@ -0,0 +1,13 @@
<template>
<SafeViewport>
<ChatApp />
</SafeViewport>
</template>
<script setup>
// ChatRoot.vue
// Kleine root-component die de ChatApp binnen de SafeViewport wrapper rendert.
import ChatApp from './ChatApp.vue';
import SafeViewport from './SafeViewport.vue';
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="safe-viewport-wrapper">
<slot />
</div>
</template>
<script setup>
// SafeViewport.vue
// Wrapper-component die de chatapplicatie omhult en de viewport-/keyboard-logica
// initialiseert via de useChatViewport composable.
// Belangrijk: de composable zelf leeft onder nginx/frontend_src/js zodat hij
// binnen dezelfde npm-package valt als de bundel (en zo @vueuse/core kan resolven).
// Daarom gebruiken we hier een relatieve pad vanuit de Vue-component naar die map.
import useChatViewport from '../js/composables/useChatViewport.js';
// Initialiseer de viewport-logica zodra deze wrapper instantiëert.
useChatViewport();
</script>
<style scoped>
.safe-viewport-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -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', {

View File

@@ -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,
});
}

View File

@@ -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",

View File

@@ -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",