diff --git a/common/services/utils/translation_services.py b/common/services/utils/translation_services.py
index 4f0e273..437ebba 100644
--- a/common/services/utils/translation_services.py
+++ b/common/services/utils/translation_services.py
@@ -1,4 +1,6 @@
import json
+import copy
+import re
from typing import Dict, Any, Optional
from flask import session
@@ -50,8 +52,8 @@ class TranslationServices:
if isinstance(config_data, str):
config_data = json.loads(config_data)
- # Maak een kopie van de originele data om te wijzigen
- translated_config = config_data.copy()
+ # Maak een deep copy van de originele data om te wijzigen en input-mutatie te vermijden
+ translated_config = copy.deepcopy(config_data)
# Haal type en versie op voor de Business Event span
config_type = config_data.get('type', 'Unknown')
@@ -65,71 +67,124 @@ class TranslationServices:
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
description_context = config_data['metadata']['description']
+ # Hulpfuncties
+ def is_nonempty_str(val: Any) -> bool:
+ return isinstance(val, str) and val.strip() != ''
+
+ def safe_translate(text: str, ctx: Optional[str]):
+ try:
+ res = cache_manager.translation_cache.get_translation(
+ text=text,
+ target_lang=target_language,
+ source_lang=source_language,
+ context=ctx
+ )
+ return res.translated_text if res else None
+ except Exception as e:
+ if current_event:
+ current_event.log_error('translation_error', {
+ 'tenant_id': tenant_id,
+ 'config_type': config_type,
+ 'config_version': config_version,
+ 'field_config': field_config,
+ 'error': str(e)
+ })
+ return None
+
+ tag_pair_pattern = re.compile(r'<([a-zA-Z][\w-]*)>[\s\S]*?<\/\1>')
+
+ def extract_tag_counts(text: str) -> Dict[str, int]:
+ counts: Dict[str, int] = {}
+ for m in tag_pair_pattern.finditer(text or ''):
+ tag = m.group(1)
+ counts[tag] = counts.get(tag, 0) + 1
+ return counts
+
+ def tags_valid(source: str, translated: str) -> bool:
+ return extract_tag_counts(source) == extract_tag_counts(translated)
+
+ # Counters
+ meta_consentRich_translated_count = 0
+ meta_aria_translated_count = 0
+ meta_inline_tags_invalid_after_translation_count = 0
+
# Loop door elk veld in de configuratie
for field_name, field_data in fields.items():
- # Vertaal name als het bestaat en niet leeg is
- if 'name' in field_data and field_data['name']:
- # Gebruik context indien opgegeven, anders description_context
+ # Vertaal name als het bestaat en niet leeg is (alleen strings)
+ if 'name' in field_data and is_nonempty_str(field_data['name']):
field_context = context if context else description_context
- translated_name = cache_manager.translation_cache.get_translation(
- text=field_data['name'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_name:
- translated_config[field_config][field_name]['name'] = translated_name.translated_text
+ t = safe_translate(field_data['name'], field_context)
+ if t:
+ translated_config[field_config][field_name]['name'] = t
- if 'title' in field_data and field_data['title']:
- # Gebruik context indien opgegeven, anders description_context
+ if 'title' in field_data and is_nonempty_str(field_data.get('title')):
field_context = context if context else description_context
- translated_title = cache_manager.translation_cache.get_translation(
- text=field_data['title'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_title:
- translated_config[field_config][field_name]['title'] = translated_title.translated_text
+ t = safe_translate(field_data['title'], field_context)
+ if t:
+ translated_config[field_config][field_name]['title'] = t
# Vertaal description als het bestaat en niet leeg is
- if 'description' in field_data and field_data['description']:
- # Gebruik context indien opgegeven, anders description_context
+ if 'description' in field_data and is_nonempty_str(field_data.get('description')):
field_context = context if context else description_context
- translated_desc = cache_manager.translation_cache.get_translation(
- text=field_data['description'],
- target_lang=target_language,
- source_lang=source_language,
- context=field_context
- )
- if translated_desc:
- translated_config[field_config][field_name]['description'] = translated_desc.translated_text
+ t = safe_translate(field_data['description'], field_context)
+ if t:
+ translated_config[field_config][field_name]['description'] = t
# Vertaal context als het bestaat en niet leeg is
- if 'context' in field_data and field_data['context']:
- translated_ctx = cache_manager.translation_cache.get_translation(
- text=field_data['context'],
- target_lang=target_language,
- source_lang=source_language,
- context=context
- )
- if translated_ctx:
- translated_config[field_config][field_name]['context'] = translated_ctx.translated_text
+ if 'context' in field_data and is_nonempty_str(field_data.get('context')):
+ t = safe_translate(field_data['context'], context)
+ if t:
+ translated_config[field_config][field_name]['context'] = t
- # vertaal allowed values als het veld bestaat en de waarden niet leeg zijn.
- if 'allowed_values' in field_data and field_data['allowed_values']:
+ # vertaal allowed_values als het veld bestaat en waarden niet leeg zijn (alleen string-items)
+ if 'allowed_values' in field_data and isinstance(field_data['allowed_values'], list) and field_data['allowed_values']:
translated_allowed_values = []
for allowed_value in field_data['allowed_values']:
- translated_allowed_value = cache_manager.translation_cache.get_translation(
- text=allowed_value,
- target_lang=target_language,
- source_lang=source_language,
- context=context
- )
- translated_allowed_values.append(translated_allowed_value.translated_text)
+ if is_nonempty_str(allowed_value):
+ t = safe_translate(allowed_value, context)
+ translated_allowed_values.append(t if t else allowed_value)
+ else:
+ translated_allowed_values.append(allowed_value)
if translated_allowed_values:
translated_config[field_config][field_name]['allowed_values'] = translated_allowed_values
+ # Vertaal meta.consentRich en meta.aria*
+ meta = field_data.get('meta')
+ if isinstance(meta, dict):
+ # consentRich
+ if is_nonempty_str(meta.get('consentRich')):
+ consent_ctx = (context if context else description_context) or ''
+ consent_ctx = f"Consent rich text with inline tags. Keep tag names intact and translate only inner text. {consent_ctx}".strip()
+ t = safe_translate(meta['consentRich'], consent_ctx)
+ if t and tags_valid(meta['consentRich'], t):
+ translated_config[field_config][field_name].setdefault('meta', {})['consentRich'] = t
+ meta_consentRich_translated_count += 1
+ else:
+ if t and not tags_valid(meta['consentRich'], t) and current_event:
+ src_counts = extract_tag_counts(meta['consentRich'])
+ dst_counts = extract_tag_counts(t)
+ current_event.log_error('inline_tags_validation_failed', {
+ 'tenant_id': tenant_id,
+ 'config_type': config_type,
+ 'config_version': config_version,
+ 'field_config': field_config,
+ 'field_name': field_name,
+ 'target_language': target_language,
+ 'source_tag_counts': src_counts,
+ 'translated_tag_counts': dst_counts
+ })
+ meta_inline_tags_invalid_after_translation_count += 1
+ # fallback: keep original (already in deep copy)
+ # aria*
+ for k, v in list(meta.items()):
+ if isinstance(k, str) and k.startswith('aria') and is_nonempty_str(v):
+ aria_ctx = (context if context else description_context) or ''
+ aria_ctx = f"ARIA label for accessibility. Short, imperative, descriptive. Form '{config_type} {config_version}', field '{field_name}'. {aria_ctx}".strip()
+ t2 = safe_translate(v, aria_ctx)
+ if t2:
+ translated_config[field_config][field_name].setdefault('meta', {})[k] = t2
+ meta_aria_translated_count += 1
+
return translated_config
@staticmethod
diff --git a/config/prompts/globals/translation_with_context/1.0.0.yaml b/config/prompts/globals/translation_with_context/1.0.0.yaml
index ec01348..a05eb85 100644
--- a/config/prompts/globals/translation_with_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_with_context/1.0.0.yaml
@@ -9,8 +9,11 @@ content: >
'{context}'
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
- Remove the triple quotes in your translation!
+ These are best practices you should follow:
+
+ - Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
+ - We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "Terms & Conditions" translates in Dutch to Gebruiksvoorwaarden
+ - Remove the triple quotes in your translation!
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
diff --git a/config/prompts/globals/translation_without_context/1.0.0.yaml b/config/prompts/globals/translation_without_context/1.0.0.yaml
index c895031..95477f9 100644
--- a/config/prompts/globals/translation_without_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_without_context/1.0.0.yaml
@@ -6,8 +6,11 @@ content: >
into '{target_language}'.
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
- Remove the triple quotes in your translation!
+ These are best practices you should follow:
+
+ - Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
+ - We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "Terms & Conditions" translates in Dutch to Gebruiksvoorwaarden
+ - Remove the triple quotes in your translation!
I only want you to return the translation. No explanation, no options. I need to be able to directly use your answer
without further interpretation. If more than one option is available, present me with the most probable one.
diff --git a/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
index 0b62127..b238615 100644
--- a/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
+++ b/config/specialist_forms/globals/MINIMAL_PERSONAL_CONTACT_FORM/1.0.0.yaml
@@ -24,6 +24,11 @@ fields:
type: "boolean"
description: "Consent"
required: true
+ meta:
+ kind: "consent"
+ consentRich: "Ik Agree with the Terms and Conditions and the Privacy Statement of Ask Eve AI"
+ ariaPrivacy: "Open privacyverklaring in a modal dialog"
+ ariaTerms: "Open algemene voorwaarden in a modal dialog"
metadata:
author: "Josako"
date_added: "2025-07-29"
diff --git a/content/changelog/1.0/1.0.0.md b/content/changelog/1.0/1.0.0.md
index ef8d9aa..752c988 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.1-alfa]
+
+### Fixed
+- TRA-76 - Send Button color changes implemented
+- TRA-72 - Translation of privacy statement and T&C
+- TRA-73 - Strange characters in Tenant Make Name
+- TRA-77 - Adapted Scroll behavior for Chat Client in Message History
+
+### Security
+- In case of vulnerabilities.
+
## [3.1.0-alfa]
### Added
diff --git a/documentation/interaction_domain.mermaid b/documentation/interaction_domain.mermaid
index 138dcc2..38410f5 100644
--- a/documentation/interaction_domain.mermaid
+++ b/documentation/interaction_domain.mermaid
@@ -101,6 +101,19 @@ erDiagram
int updated_by FK
}
+ EVE_AI_DATA_CAPSULE {
+ int id PK
+ int chat_session_id FK
+ string type
+ string type_version
+ jsonb configuration
+ jsonb data
+ datetime created_at
+ int created_by FK
+ datetime updated_at
+ int updated_by FK
+ }
+
DISPATCHER {
int id PK
string name
@@ -188,24 +201,28 @@ erDiagram
%% Main conversation flow
USER ||--o{ CHAT_SESSION : "has many"
CHAT_SESSION ||--o{ INTERACTION : "has many"
+ CHAT_SESSION ||--o{ EVE_AI_DATA_CAPSULE : "has many"
SPECIALIST ||--o{ INTERACTION : "processes"
-
+
%% Specialist composition (EveAI components)
SPECIALIST ||--o{ EVE_AI_AGENT : "has many"
SPECIALIST ||--o{ EVE_AI_TASK : "has many"
SPECIALIST ||--o{ EVE_AI_TOOL : "has many"
-
+
%% Specialist connections
SPECIALIST ||--o{ SPECIALIST_RETRIEVER : "uses retrievers"
RETRIEVER ||--o{ SPECIALIST_RETRIEVER : "used by specialists"
-
+
SPECIALIST ||--o{ SPECIALIST_DISPATCHER : "uses dispatchers"
DISPATCHER ||--o{ SPECIALIST_DISPATCHER : "used by specialists"
-
+
%% Interaction results
INTERACTION ||--o{ INTERACTION_EMBEDDING : "references embeddings"
EMBEDDING ||--o{ INTERACTION_EMBEDDING : "used in interactions"
-
+
%% Magic links for specialist access
SPECIALIST ||--o{ SPECIALIST_MAGIC_LINK : "has magic links"
- TENANT_MAKE ||--o{ SPECIALIST_MAGIC_LINK : "branded links"
\ No newline at end of file
+ TENANT_MAKE ||--o{ SPECIALIST_MAGIC_LINK : "branded links"
+
+ %% User relationships for audit trails
+ USER ||--o{ EVE_AI_DATA_CAPSULE : "created/updated by"
\ No newline at end of file
diff --git a/eveai_chat_client/static/assets/css/chat-components.css b/eveai_chat_client/static/assets/css/chat-components.css
index db868c8..96ed506 100644
--- a/eveai_chat_client/static/assets/css/chat-components.css
+++ b/eveai_chat_client/static/assets/css/chat-components.css
@@ -1,45 +1,3 @@
-
-/* Chat App Container Layout */
-.chat-app-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
- min-height: 0; /* Belangrijk voor flexbox overflow */
- padding: 20px; /* Algemene padding voor alle kanten */
- box-sizing: border-box;
-}
-
-/* Gemeenschappelijke container voor consistente breedte */
-.chat-component-container {
- width: 100%;
- max-width: 1000px; /* Optimale breedte */
- margin-left: auto;
- margin-right: auto;
- display: flex;
- flex-direction: column;
- flex: 1; /* Neemt beschikbare verticale ruimte in */
-}
-
-/* Message Area - neemt alle beschikbare ruimte */
-.chat-messages-area {
- flex: 1; /* Neemt alle beschikbare ruimte */
- overflow: hidden; /* Voorkomt dat het groter wordt dan container */
- display: flex;
- flex-direction: column;
- min-height: 0; /* Belangrijk voor nested flexbox */
- margin-bottom: 20px; /* Ruimte tussen messages en input */
- border-radius: 15px;
- background: var(--history-background);
- backdrop-filter: blur(10px);
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
- width: 100%;
- max-width: 1000px; /* Optimale breedte */
- margin-left: auto;
- margin-right: auto; /* Horizontaal centreren */
- align-self: center; /* Extra centrering in flexbox context */
-}
-
/* Chat Input - altijd onderaan */
.chat-input-area {
flex: none; /* Neemt alleen benodigde ruimte */
@@ -56,14 +14,6 @@
align-self: center; /* Extra centrering in flexbox context */
}
-.chat-messages {
- flex: 1;
- overflow-y: auto;
- padding-right: 10px; /* Ruimte voor scrollbar */
- margin-right: -10px; /* Compenseer voor scrollbar */
- scroll-behavior: smooth;
-}
-
/* Chat Input styling */
.chat-input {
diff --git a/eveai_chat_client/static/assets/css/chat.css b/eveai_chat_client/static/assets/css/chat.css
index 5d78776..9449b6d 100644
--- a/eveai_chat_client/static/assets/css/chat.css
+++ b/eveai_chat_client/static/assets/css/chat.css
@@ -99,15 +99,6 @@ body {
width: 100%;
}
-/* Chat layout */
-.chat-container {
- display: flex;
- height: 100%;
- flex: 1;
- flex-direction: column;
- min-height: 0;
-}
-
.sidebar {
width: 280px;
background-color: var(--sidebar-background);
diff --git a/eveai_chat_client/static/assets/vue-components/ChatApp.vue b/eveai_chat_client/static/assets/vue-components/ChatApp.vue
index b41cc5f..7dc6619 100644
--- a/eveai_chat_client/static/assets/vue-components/ChatApp.vue
+++ b/eveai_chat_client/static/assets/vue-components/ChatApp.vue
@@ -549,20 +549,40 @@ export default {
diff --git a/eveai_chat_client/static/assets/vue-components/FormField.vue b/eveai_chat_client/static/assets/vue-components/FormField.vue
index 336de27..4a2b130 100644
--- a/eveai_chat_client/static/assets/vue-components/FormField.vue
+++ b/eveai_chat_client/static/assets/vue-components/FormField.vue
@@ -104,14 +104,16 @@
>
{{ field.name }}
-
-
- {{ texts.consentPrefix }}
- {{ texts.privacyLink }}
- {{ texts.consentMiddle }}
- {{ texts.termsLink }}
- {{ texts.consentSuffix }}
-
+
+
*
@@ -180,9 +182,11 @@