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