diff --git a/app/eveai_chat_client/templates/chat.html b/app/eveai_chat_client/templates/chat.html
deleted file mode 100644
index 07f3f52..0000000
--- a/app/eveai_chat_client/templates/chat.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-{% extends "base.html" %}
-
-{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
-
-{% block head %}
diff --git a/common/models/user.py b/common/models/user.py
index 7c895a1..aa0ad27 100644
--- a/common/models/user.py
+++ b/common/models/user.py
@@ -26,9 +26,6 @@ class Tenant(db.Model):
timezone = db.Column(db.String(50), nullable=True, default='UTC')
type = db.Column(db.String(20), nullable=True, server_default='Active')
- # language information
- default_language = db.Column(db.String(2), nullable=True)
-
# Entitlements
currency = db.Column(db.String(20), nullable=True)
storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
@@ -61,7 +58,6 @@ class Tenant(db.Model):
'website': self.website,
'timezone': self.timezone,
'type': self.type,
- 'default_language': self.default_language,
'currency': self.currency,
'default_tenant_make_id': self.default_tenant_make_id,
}
@@ -186,6 +182,7 @@ class TenantMake(db.Model):
active = db.Column(db.Boolean, nullable=False, default=True)
website = db.Column(db.String(255), nullable=True)
logo_url = db.Column(db.String(255), nullable=True)
+ default_language = db.Column(db.String(2), nullable=True)
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
# Chat customisation options
@@ -209,6 +206,8 @@ class TenantMake(db.Model):
'website': self.website,
'logo_url': self.logo_url,
'chat_customisation_options': self.chat_customisation_options,
+ 'allowed_languages': self.allowed_languages,
+ 'default_language': self.default_language,
}
@@ -327,7 +326,7 @@ class TranslationCache(db.Model):
cache_key = db.Column(db.String(16), primary_key=True)
source_text = db.Column(db.Text, nullable=False)
translated_text = db.Column(db.Text, nullable=False)
- source_language = db.Column(db.String(2), nullable=False)
+ source_language = db.Column(db.String(2), nullable=True)
target_language = db.Column(db.String(2), nullable=False)
context = db.Column(db.Text, nullable=True)
diff --git a/common/services/utils/translation_services.py b/common/services/utils/translation_services.py
index d35fae0..0ef3186 100644
--- a/common/services/utils/translation_services.py
+++ b/common/services/utils/translation_services.py
@@ -1,43 +1,108 @@
-import xxhash
import json
+from typing import Dict, Any, Optional
+from common.extensions import cache_manager
+from common.utils.business_event import BusinessEvent
+from common.utils.business_event_context import current_event
-from langchain_core.output_parsers import StrOutputParser
-from langchain_core.prompts import ChatPromptTemplate
-from langchain_core.runnables import RunnablePassthrough
+class TranslationServices:
-from common.langchain.persistent_llm_metrics_handler import PersistentLLMMetricsHandler
-from common.utils.model_utils import get_template, replace_variable_in_template
+ @staticmethod
+ def translate_config(config_data: Dict[str, Any], field_config: str, target_language: str, source_language: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]:
+ """
+ Vertaalt een configuratie op basis van een veld-configuratie.
-class TranslationService:
- def __init__(self, tenant_id):
- self.tenant_id = tenant_id
+ Args:
+ config_data: Een dictionary of JSON (die dan wordt geconverteerd naar een dictionary) met configuratiegegevens
+ field_config: De naam van een veld-configuratie (bijv. 'fields')
+ target_language: De taal waarnaar vertaald moet worden
+ source_language: Optioneel, de brontaal van de configuratie
+ context: Optioneel, een specifieke context voor de vertaling
- def translate_text(self, text_to_translate: str, target_lang: str, source_lang: str = None, context: str = None) -> tuple[
- str, dict[str, int | float]]:
- prompt_params = {
- "text_to_translate": text_to_translate,
- "target_lang": target_lang,
- }
- if context:
- template, llm = get_template("translation_with_context")
- prompt_params["context"] = context
- else:
- template, llm = get_template("translation_without_context")
+ Returns:
+ Een dictionary met de vertaalde configuratie
+ """
+ # Zorg ervoor dat we een dictionary hebben
+ if isinstance(config_data, str):
+ config_data = json.loads(config_data)
- # Add a metrics handler to capture usage
+ # Maak een kopie van de originele data om te wijzigen
+ translated_config = config_data.copy()
- metrics_handler = PersistentLLMMetricsHandler()
- existing_callbacks = llm.callbacks
- llm.callbacks = existing_callbacks + [metrics_handler]
+ # Haal type en versie op voor de Business Event span
+ config_type = config_data.get('type', 'Unknown')
+ config_version = config_data.get('version', 'Unknown')
+ span_name = f"{config_type}-{config_version}-{field_config}"
- translation_prompt = ChatPromptTemplate.from_template(template)
+ # Start een Business Event context
+ with BusinessEvent('Config Translation Service', 0):
+ with current_event.create_span(span_name):
+ # Controleer of de gevraagde veld-configuratie bestaat
+ if field_config in config_data:
+ fields = config_data[field_config]
- setup = RunnablePassthrough()
+ # Haal description uit metadata voor context als geen context is opgegeven
+ description_context = ""
+ if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
+ description_context = config_data['metadata']['description']
- chain = (setup | translation_prompt | llm | StrOutputParser())
+ # 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
+ 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
- translation = chain.invoke(prompt_params)
+ if 'title' in field_data and field_data['title']:
+ # Gebruik context indien opgegeven, anders description_context
+ 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
- metrics = metrics_handler.get_metrics()
+ # 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
+ 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
- return translation, metrics
\ No newline at end of file
+ # 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
+
+ return translated_config
+
+ @staticmethod
+ def translate(text: str, target_language: str, source_language: Optional[str] = None,
+ context: Optional[str] = None)-> str:
+ with BusinessEvent('Translation Service', 0):
+ with current_event.create_span('Translation'):
+ translation_cache = cache_manager.translation_cache.get_translation(text, target_language,
+ source_language, context)
+ return translation_cache.translated_text
\ No newline at end of file
diff --git a/common/utils/cache/regions.py b/common/utils/cache/regions.py
index 4b51149..cfe48e0 100644
--- a/common/utils/cache/regions.py
+++ b/common/utils/cache/regions.py
@@ -42,7 +42,7 @@ def create_cache_regions(app):
# Region for model-related caching (ModelVariables etc)
model_region = make_region(name='eveai_model').configure(
'dogpile.cache.redis',
- arguments=redis_config,
+ arguments={**redis_config, 'db': 6},
replace_existing_backend=True
)
regions['eveai_model'] = model_region
diff --git a/common/utils/cache/translation_cache.py b/common/utils/cache/translation_cache.py
index 5e1e409..e9d1d5f 100644
--- a/common/utils/cache/translation_cache.py
+++ b/common/utils/cache/translation_cache.py
@@ -1,19 +1,25 @@
import json
+import re
from typing import Dict, Any, Optional
from datetime import datetime as dt, timezone as tz
import xxhash
from flask import current_app
-from sqlalchemy import and_
+from langchain_core.output_parsers import StrOutputParser
+from langchain_core.prompts import ChatPromptTemplate
+from langchain_core.runnables import RunnablePassthrough
from sqlalchemy.inspection import inspect
+from common.langchain.persistent_llm_metrics_handler import PersistentLLMMetricsHandler
+from common.utils.business_event_context import current_event
from common.utils.cache.base import CacheHandler, T
from common.extensions import db
from common.models.user import TranslationCache
-from common.services.utils.translation_services import TranslationService
from flask_security import current_user
+from common.utils.model_utils import get_template
+
class TranslationCacheHandler(CacheHandler[TranslationCache]):
"""Handles caching of translations with fallback to database and external translation service"""
@@ -62,13 +68,33 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
setattr(translation, column.name, value)
+ current_app.logger.debug(f"Translation Cache Retrieved: {translation}")
+ metrics = {
+ 'total_tokens': translation.prompt_tokens + translation.completion_tokens,
+ 'prompt_tokens': translation.prompt_tokens,
+ 'completion_tokens': translation.completion_tokens,
+ 'time_elapsed': 0,
+ 'interaction_type': 'LLM'
+ }
+ current_event.log_llm_metrics(metrics)
+
return translation
- def _should_cache(self, value: TranslationCache) -> bool:
+ def _should_cache(self, value) -> bool:
"""Validate if the translation should be cached"""
- return value is not None and value.cache_key is not None
+ if value is None:
+ return False
- def get_translation(self, text: str, target_lang: str, source_lang:str=None, context: str=None) -> Optional[TranslationCache]:
+ # Handle both TranslationCache objects and serialized data (dict)
+ if isinstance(value, TranslationCache):
+ return value.cache_key is not None
+ elif isinstance(value, dict):
+ return value.get('cache_key') is not None
+
+ return False
+
+ def get_translation(self, text: str, target_lang: str, source_lang: str = None, context: str = None) -> Optional[
+ TranslationCache]:
"""
Get the translation for a text in a specific language
@@ -81,26 +107,33 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
Returns:
TranslationCache instance if found, None otherwise
"""
+ if not context:
+ context = 'No context provided.'
+ current_app.logger.debug(f"Getting translation for text: {text[:10]}..., target_lang: {target_lang}, source_lang: {source_lang}, context: {context[:10]}...")
- def creator_func(text: str, target_lang: str, source_lang: str=None, context: str=None) -> Optional[TranslationCache]:
- # Generate cache key based on inputs
- cache_key = self._generate_cache_key(text, target_lang, source_lang, context)
-
+ def creator_func(hash_key: str) -> Optional[TranslationCache]:
# Check if translation already exists in database
- existing_translation = db.session.query(TranslationCache).filter_by(cache_key=cache_key).first()
+ existing_translation = db.session.query(TranslationCache).filter_by(cache_key=hash_key).first()
if existing_translation:
# Update last used timestamp
existing_translation.last_used_at = dt.now(tz=tz.utc)
+ metrics = {
+ 'total_tokens': existing_translation.prompt_tokens + existing_translation.completion_tokens,
+ 'prompt_tokens': existing_translation.prompt_tokens,
+ 'completion_tokens': existing_translation.completion_tokens,
+ 'time_elapsed': 0,
+ 'interaction_type': 'LLM'
+ }
+ current_app.logger.debug(f"Found existing translation in DB: {existing_translation.cache_key}")
+ current_app.logger.debug(f"Metrics: {metrics}")
+ current_event.log_llm_metrics(metrics)
db.session.commit()
return existing_translation
# Translation not found in DB, need to create it
- # Initialize translation service
- translation_service = TranslationService(getattr(current_app, 'tenant_id', None))
-
# Get the translation and metrics
- translated_text, metrics = translation_service.translate_text(
+ translated_text, metrics = self.translate_text(
text_to_translate=text,
target_lang=target_lang,
source_lang=source_lang,
@@ -109,10 +142,10 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
# Create new translation cache record
new_translation = TranslationCache(
- cache_key=cache_key,
+ cache_key=hash_key,
source_text=text,
translated_text=translated_text,
- source_language=source_lang or 'auto',
+ source_language=source_lang,
target_language=target_lang,
context=context,
prompt_tokens=metrics.get('prompt_tokens', 0),
@@ -130,7 +163,12 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
return new_translation
- return self.get(creator_func, text=text, target_lang=target_lang, source_lang=source_lang, context=context)
+ # Generate the hash key using your existing method
+ hash_key = self._generate_cache_key(text, target_lang, source_lang, context)
+ current_app.logger.debug(f"Generated hash key: {hash_key}")
+
+ # Pass the hash_key to the get method
+ return self.get(creator_func, hash_key=hash_key)
def invalidate_tenant_translations(self, tenant_id: int):
"""Invalidate cached translations for specific tenant"""
@@ -148,6 +186,41 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
cache_string = json.dumps(cache_data, sort_keys=True, ensure_ascii=False)
return xxhash.xxh64(cache_string.encode('utf-8')).hexdigest()
+ def translate_text(self, text_to_translate: str, target_lang: str, source_lang: str = None, context: str = None) \
+ -> tuple[str, dict[str, int | float]]:
+ target_language = current_app.config['SUPPORTED_LANGUAGE_ISO639_1_LOOKUP'][target_lang]
+ current_app.logger.debug(f"Target language: {target_language}")
+ prompt_params = {
+ "text_to_translate": text_to_translate,
+ "target_language": target_language,
+ }
+ if context:
+ template, llm = get_template("translation_with_context")
+ prompt_params["context"] = context
+ else:
+ template, llm = get_template("translation_without_context")
+
+ # Add a metrics handler to capture usage
+
+ metrics_handler = PersistentLLMMetricsHandler()
+ existing_callbacks = llm.callbacks
+ llm.callbacks = existing_callbacks + [metrics_handler]
+
+ translation_prompt = ChatPromptTemplate.from_template(template)
+
+ setup = RunnablePassthrough()
+
+ chain = (setup | translation_prompt | llm | StrOutputParser())
+
+ translation = chain.invoke(prompt_params)
+
+ # Remove double square brackets from translation
+ translation = re.sub(r'\[\[(.*?)\]\]', r'\1', translation)
+
+ metrics = metrics_handler.get_metrics()
+
+ return translation, metrics
+
def register_translation_cache_handlers(cache_manager) -> None:
"""Register translation cache handlers with cache manager"""
cache_manager.register_handler(
diff --git a/common/utils/chat_utils.py b/common/utils/chat_utils.py
index 4a3fe32..b8a0cd2 100644
--- a/common/utils/chat_utils.py
+++ b/common/utils/chat_utils.py
@@ -31,7 +31,6 @@ def get_default_chat_customisation(tenant_customisation=None):
'markdown_background_color': 'transparent',
'markdown_text_color': '#ffffff',
'sidebar_markdown': '',
- 'welcome_message': 'Hello! How can I help you today?',
}
# If no tenant customization is provided, return the defaults
diff --git a/common/utils/security.py b/common/utils/security.py
index fd23676..b8946fc 100644
--- a/common/utils/security.py
+++ b/common/utils/security.py
@@ -12,7 +12,6 @@ from datetime import datetime as dt, timezone as tz
def set_tenant_session_data(sender, user, **kwargs):
tenant = Tenant.query.filter_by(id=user.tenant_id).first()
session['tenant'] = tenant.to_dict()
- session['default_language'] = tenant.default_language
partner = Partner.query.filter_by(tenant_id=user.tenant_id).first()
if partner:
session['partner'] = partner.to_dict()
diff --git a/config/config.py b/config/config.py
index f85a6fc..72f2d16 100644
--- a/config/config.py
+++ b/config/config.py
@@ -66,7 +66,6 @@ class Config(object):
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
# supported languages
- SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi']
SUPPORTED_LANGUAGE_DETAILS = {
"English": {
"iso 639-1": "en",
@@ -148,7 +147,10 @@ class Config(object):
},
}
+ # Afgeleide taalconstanten
+ SUPPORTED_LANGUAGES = [lang_details["iso 639-1"] for lang_details in SUPPORTED_LANGUAGE_DETAILS.values()]
SUPPORTED_LANGUAGES_FULL = list(SUPPORTED_LANGUAGE_DETAILS.keys())
+ SUPPORTED_LANGUAGE_ISO639_1_LOOKUP = {lang_details["iso 639-1"]: lang_name for lang_name, lang_details in SUPPORTED_LANGUAGE_DETAILS.items()}
# supported currencies
SUPPORTED_CURRENCIES = ['€', '$']
@@ -293,6 +295,8 @@ class DevConfig(Config):
CHAT_WORKER_CACHE_URL = f'{REDIS_BASE_URI}/4'
# specialist execution pub/sub Redis Settings
SPECIALIST_EXEC_PUBSUB = f'{REDIS_BASE_URI}/5'
+ # eveai_model cache Redis setting
+ MODEL_CACHE_URL = f'{REDIS_BASE_URI}/6'
# Unstructured settings
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 b7c5be5..21fd5eb 100644
--- a/config/prompts/globals/translation_with_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_with_context/1.0.0.yaml
@@ -1,9 +1,16 @@
version: "1.0.0"
content: >
- You are a top translator. We need you to translate {text_to_translate} into {target_language}, taking into account
- this context:
+ You are a top translator. We need you to translate (in between triple quotes)
- {context}
+ '''{text_to_translate}'''
+
+ into '{target_language}', taking
+ into account this context:
+
+ '{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!
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 08d2990..6071261 100644
--- a/config/prompts/globals/translation_without_context/1.0.0.yaml
+++ b/config/prompts/globals/translation_without_context/1.0.0.yaml
@@ -1,6 +1,13 @@
version: "1.0.0"
content: >
- You are a top translator. We need you to translate {text_to_translate} into {target_language}.
+ You are a top translator. We need you to translate (in between triple quotes)
+
+ '''{text_to_translate}'''
+
+ 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!
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/PERSONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml
index b7f5b5f..3f20fd9 100644
--- a/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml
+++ b/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml
@@ -8,6 +8,7 @@ fields:
description: "Your name"
type: "str"
required: true
+# It is possible to also add a field 'context'. It allows you to provide an elaborate piece of information.
email:
name: "Email"
type: "str"
@@ -17,7 +18,6 @@ fields:
name: "Phone Number"
type: "str"
description: "Your Phone Number"
- context: "Een kleine test om te zien of we context kunnen doorgeven en tonen"
required: true
address:
name: "Address"
@@ -44,3 +44,8 @@ fields:
type: "boolean"
description: "Consent"
required: true
+metadata:
+ author: "Josako"
+ date_added: "2025-06-18"
+ changes: "Initial Version"
+ description: "Personal Contact Form"
diff --git a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml
index cbcf484..b894cb3 100644
--- a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml
+++ b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml
@@ -53,3 +53,8 @@ fields:
type: "bool"
description: "Consent"
required: true
+metadata:
+ author: "Josako"
+ date_added: "2025-06-18"
+ changes: "Initial Version"
+ description: "Professional Contact Form"
diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py
index 54bb883..7258713 100644
--- a/eveai_app/__init__.py
+++ b/eveai_app/__init__.py
@@ -106,20 +106,20 @@ def create_app(config_file=None):
from flask_login import current_user
import datetime
- app.logger.debug(f"Before request - URL: {request.url}")
- app.logger.debug(f"Before request - Session permanent: {session.permanent}")
+ # app.logger.debug(f"Before request - URL: {request.url}")
+ # app.logger.debug(f"Before request - Session permanent: {session.permanent}")
# Log session expiry tijd als deze bestaat
if current_user.is_authenticated:
# Controleer of sessie permanent is (nodig voor PERMANENT_SESSION_LIFETIME)
if not session.permanent:
session.permanent = True
- app.logger.debug("Session marked as permanent (enables 60min timeout)")
+ # app.logger.debug("Session marked as permanent (enables 60min timeout)")
# Log wanneer sessie zou verlopen
- if '_permanent' in session:
- expires_at = datetime.datetime.now() + app.permanent_session_lifetime
- app.logger.debug(f"Session will expire at: {expires_at} (60 min from now)")
+ # if '_permanent' in session:
+ # expires_at = datetime.datetime.now() + app.permanent_session_lifetime
+ # app.logger.debug(f"Session will expire at: {expires_at} (60 min from now)")
@app.route('/debug/session')
def debug_session():
diff --git a/eveai_app/views/basic_forms.py b/eveai_app/views/basic_forms.py
index 7fdc3c3..5f50c68 100644
--- a/eveai_app/views/basic_forms.py
+++ b/eveai_app/views/basic_forms.py
@@ -16,7 +16,6 @@ class SessionDefaultsForm(FlaskForm):
# Tenant Defaults
tenant_name = StringField('Tenant Name', validators=[DataRequired()])
- default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# Partner Defaults
partner_name = StringField('Partner Name', validators=[DataRequired()])
diff --git a/eveai_app/views/basic_views.py b/eveai_app/views/basic_views.py
index 3f91cef..eefba77 100644
--- a/eveai_app/views/basic_views.py
+++ b/eveai_app/views/basic_views.py
@@ -59,7 +59,6 @@ def session_defaults():
form = SessionDefaultsForm()
if form.validate_on_submit():
- session['default_language'] = form.default_language.data
if form.catalog.data:
catalog_id = int(form.catalog.data)
catalog = tenant_session.query(Catalog).get(catalog_id)
diff --git a/eveai_app/views/dynamic_form_base.py b/eveai_app/views/dynamic_form_base.py
index cb135ec..fa6b5bf 100644
--- a/eveai_app/views/dynamic_form_base.py
+++ b/eveai_app/views/dynamic_form_base.py
@@ -453,9 +453,6 @@ class DynamicFormBase(FlaskForm):
else:
render_kw['class'] = 'color-field'
-
- current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
-
# Create the field
field_kwargs.update({
'label': label,
diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py
index 6fa7b7d..4eadc45 100644
--- a/eveai_app/views/user_forms.py
+++ b/eveai_app/views/user_forms.py
@@ -18,8 +18,6 @@ class TenantForm(FlaskForm):
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
- # language fields
- default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone
@@ -32,8 +30,6 @@ class TenantForm(FlaskForm):
def __init__(self, *args, **kwargs):
super(TenantForm, self).__init__(*args, **kwargs)
- # initialise language fields
- self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
# initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone
@@ -53,8 +49,6 @@ class EditTenantForm(FlaskForm):
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
- # language fields
- default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone
@@ -69,8 +63,6 @@ class EditTenantForm(FlaskForm):
def __init__(self, *args, **kwargs):
super(EditTenantForm, self).__init__(*args, **kwargs)
- # initialise language fields
- self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
# initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone
@@ -212,14 +204,17 @@ class EditTenantMakeForm(DynamicFormBase):
active = BooleanField('Active', validators=[Optional()], default=True)
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)])
+ default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[Optional()])
def __init__(self, *args, **kwargs):
super(EditTenantMakeForm, self).__init__(*args, **kwargs)
# Initialiseer de taalopties met taalcodes en vlaggen
lang_details = current_app.config['SUPPORTED_LANGUAGE_DETAILS']
- self.allowed_languages.choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}")
- for name, details in lang_details.items()]
+ choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}")
+ for name, details in lang_details.items()]
+ self.allowed_languages.choices = choices
+ self.default_language.choices = choices
diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py
index 5810587..d68be50 100644
--- a/eveai_app/views/user_views.py
+++ b/eveai_app/views/user_views.py
@@ -309,7 +309,6 @@ def handle_tenant_selection():
# set tenant information in the session
session['tenant'] = the_tenant.to_dict()
- session['default_language'] = the_tenant.default_language
# remove catalog-related items from the session
session.pop('catalog_id', None)
session.pop('catalog_name', None)
@@ -706,8 +705,9 @@ def edit_tenant_make(tenant_make_id):
form = EditTenantMakeForm(request.form, obj=tenant_make)
# Initialiseer de allowed_languages selectie met huidige waarden
- if tenant_make.allowed_languages:
- form.allowed_languages.data = tenant_make.allowed_languages
+ if request.method == 'GET':
+ if tenant_make.allowed_languages:
+ form.allowed_languages.data = tenant_make.allowed_languages
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
@@ -717,7 +717,9 @@ def edit_tenant_make(tenant_make_id):
form.populate_obj(tenant_make)
tenant_make.chat_customisation_options = form.get_dynamic_data("configuration")
# Verwerk allowed_languages als array
+ current_app.logger.debug(f"Allowed languages: {form.allowed_languages.data}")
tenant_make.allowed_languages = form.allowed_languages.data if form.allowed_languages.data else None
+ current_app.logger.debug(f"Updated allowed languages: {tenant_make.allowed_languages}")
# Update logging information
update_logging_information(tenant_make, dt.now(tz.utc))
diff --git a/eveai_chat_client/__init__.py b/eveai_chat_client/__init__.py
index 7a3df0e..8a818e0 100644
--- a/eveai_chat_client/__init__.py
+++ b/eveai_chat_client/__init__.py
@@ -112,3 +112,5 @@ def register_cache_handlers(app):
register_config_cache_handlers(cache_manager)
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
register_specialist_cache_handlers(cache_manager)
+ from common.utils.cache.translation_cache import register_translation_cache_handlers
+ register_translation_cache_handlers(cache_manager)
diff --git a/eveai_chat_client/static/assets/css/chat.css b/eveai_chat_client/static/assets/css/chat.css
index b97d2fc..f16f9b8 100644
--- a/eveai_chat_client/static/assets/css/chat.css
+++ b/eveai_chat_client/static/assets/css/chat.css
@@ -11,10 +11,85 @@
--spacing: 16px;
}
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
+/* App container layout */
+.app-container {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+}
+
+/* Sidebar styling */
+.sidebar {
+ width: 300px;
+ background-color: var(--sidebar-background);
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+
+.sidebar-logo {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.sidebar-logo img {
+ max-width: 100%;
+ max-height: 100px;
+}
+
+.sidebar-make-name {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.sidebar-explanation {
+ margin-top: 20px;
+ overflow-y: auto;
+ background-color: var(--markdown-background-color);
+ color: var(--markdown-text-color);
+ padding: 10px;
+ border-radius: 5px;
+}
+
+/* Ensure all elements in the markdown content inherit the text color */
+.sidebar-explanation * {
+ color: inherit;
+}
+
+/* Style links in the markdown content */
+.sidebar-explanation a {
+ color: var(--primary-color);
+ text-decoration: underline;
+}
+
+/* Style lists in markdown content */
+.sidebar-explanation ul,
+.sidebar-explanation ol {
+ padding-left: 20px;
+ margin: 10px 0;
+}
+
+.sidebar-explanation li {
+ margin-bottom: 5px;
+}
+
+.sidebar-explanation ul li {
+ list-style-type: disc;
+}
+
+.sidebar-explanation ol li {
+ list-style-type: decimal;
+}
+
+.content-area {
+ flex: 1;
+ background: linear-gradient(135deg, var(--gradient-start-color), var(--gradient-end-color));
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
}
body {
@@ -35,11 +110,14 @@ body {
.chat-container {
display: flex;
height: 100%;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
}
.sidebar {
width: 280px;
- background-color: var(--sidebar-color);
+ background-color: var(--sidebar-background);
border-right: 1px solid rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
@@ -98,6 +176,35 @@ body {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
+/* Indicator voor taalwijziging */
+.language-change-indicator {
+ background-color: rgba(var(--primary-color-rgb, 0, 123, 255), 0.2);
+ color: white;
+ padding: 5px 8px;
+ margin-bottom: 10px;
+ border-radius: 4px;
+ font-size: 0.9em;
+ text-align: center;
+ animation: fadeInOut 3s ease-in-out;
+}
+
+@keyframes fadeInOut {
+ 0% { opacity: 0; }
+ 10% { opacity: 1; }
+ 90% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+.language-change-indicator.success {
+ background: #d4edda;
+ color: #155724;
+}
+
+.language-change-indicator.error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
/* .chat-messages wordt nu gedefinieerd in chat-components.css */
/* .message wordt nu gedefinieerd in chat-components.css */
@@ -164,4 +271,4 @@ body {
/* .btn-primary wordt nu gedefinieerd in chat-components.css */
-/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
\ No newline at end of file
+/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
diff --git a/eveai_chat_client/static/assets/css/language-selector.css b/eveai_chat_client/static/assets/css/language-selector.css
new file mode 100644
index 0000000..8776443
--- /dev/null
+++ b/eveai_chat_client/static/assets/css/language-selector.css
@@ -0,0 +1,50 @@
+/* Styling voor de taalselector */
+
+.sidebar-language-section {
+ padding: 10px 15px;
+ margin-bottom: 15px;
+}
+
+#language-selector-container {
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 5px;
+ margin: 10px 0;
+}
+
+#language-selector-container label {
+ margin-bottom: 5px;
+ color: var(--sidebar-color);
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.language-selector {
+ padding: 8px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+ background-color: rgba(0, 0, 0, 0.2);
+ color: var(--sidebar-color);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ width: 100%;
+}
+
+.language-selector:hover {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+.language-selector:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.3);
+}
+
+.language-selector option {
+ background-color: #2c3e50;
+ color: white;
+ padding: 8px;
+}
diff --git a/eveai_chat_client/static/assets/js/chat-app.js b/eveai_chat_client/static/assets/js/chat-app.js
index f0413a3..0fe302c 100644
--- a/eveai_chat_client/static/assets/js/chat-app.js
+++ b/eveai_chat_client/static/assets/js/chat-app.js
@@ -5,6 +5,7 @@ import { DynamicForm } from '/static/assets/js/components/DynamicForm.js';
import { ChatMessage } from '/static/assets/js/components/ChatMessage.js';
import { MessageHistory } from '/static/assets/js/components/MessageHistory.js';
import { ProgressTracker } from '/static/assets/js/components/ProgressTracker.js';
+import { LanguageSelector } from '/static/assets/js/components/LanguageSelector.js';
// Maak componenten globaal beschikbaar voordat andere componenten worden geladen
window.DynamicForm = DynamicForm;
@@ -33,9 +34,15 @@ export const ChatApp = {
// Maak een lokale kopie van de chatConfig om undefined errors te voorkomen
const chatConfig = window.chatConfig || {};
const settings = chatConfig.settings || {};
+ const initialLanguage = chatConfig.language || 'nl';
+ const originalExplanation = chatConfig.explanation || '';
return {
- // Base template data (keeping existing functionality)
+ // Taal gerelateerde data
+ currentLanguage: '',
+ supportedLanguageDetails: chatConfig.supportedLanguageDetails || {},
+ allowedLanguages: chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
+ originalExplanation: originalExplanation,
explanation: chatConfig.explanation || '',
// Chat-specific data
@@ -164,6 +171,14 @@ export const ChatApp = {
// Keyboard shortcuts
document.addEventListener('keydown', this.handleGlobalKeydown);
+
+ // Luister naar taalwijzigingen via custom events
+ document.addEventListener('language-changed', (event) => {
+ if (event.detail && event.detail.language) {
+ console.log('ChatApp received language-changed event:', event.detail.language);
+ this.handleLanguageChange(event.detail.language);
+ }
+ });
},
cleanup() {
@@ -171,6 +186,113 @@ export const ChatApp = {
document.removeEventListener('keydown', this.handleGlobalKeydown);
},
+ // Taal gerelateerde functionaliteit
+ handleLanguageChange(newLanguage) {
+ if (this.currentLanguage !== newLanguage) {
+ console.log(`ChatApp: Taal gewijzigd van ${this.currentLanguage} naar ${newLanguage}`);
+ this.currentLanguage = newLanguage;
+
+ // Vertaal de sidebar
+ this.translateSidebar(newLanguage);
+
+ // Sla de taalvoorkeur op voor toekomstige API calls
+ this.storeLanguagePreference(newLanguage);
+
+ // Stuur language-changed event voor andere componenten (zoals ChatInput)
+ // Dit wordt gedaan via het event systeem, waardoor we geen directe referentie nodig hebben
+ const event = new CustomEvent('language-changed', {
+ detail: { language: newLanguage }
+ });
+ document.dispatchEvent(event);
+ }
+ },
+
+ // Maak de handleLanguageChange methode toegankelijk van buitenaf
+ // Deze functie wordt opgeroepen door het externe LanguageSelector component
+ __handleExternalLanguageChange(newLanguage) {
+ this.handleLanguageChange(newLanguage);
+ },
+
+ storeLanguagePreference(language) {
+ // Sla op in localStorage voor persistentie
+ localStorage.setItem('preferredLanguage', language);
+
+ // Update chatConfig voor toekomstige API calls
+ if (window.chatConfig) {
+ window.chatConfig.language = language;
+ }
+
+ console.log(`Taalvoorkeur opgeslagen: ${language}`);
+ },
+
+ async translateSidebar(language) {
+ console.log(`Sidebar wordt vertaald naar: ${language}`);
+
+ // Haal de originele tekst op
+ const originalText = this.originalExplanation || this.explanation;
+
+ try {
+ // Controleer of TranslationClient beschikbaar is
+ if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
+ console.error('TranslationClient.translate is niet beschikbaar');
+ this.showTranslationIndicator(language, 'Vertaling niet beschikbaar', false);
+ return;
+ }
+
+ // Toon loading indicator
+ this.showTranslationIndicator(language, 'Bezig met vertalen...');
+ console.log('API prefix voor vertaling:', this.apiPrefix);
+
+ // Gebruik TranslationClient met de juiste parameters
+ const response = await window.TranslationClient.translate(
+ originalText,
+ language,
+ null, // source_lang (auto-detect)
+ 'sidebar_explanation', // context
+ this.apiPrefix // API prefix voor tenant routing
+ );
+
+ if (response.success) {
+ // Update de explanation variabele
+ console.log('Translated text: ' + response.translated_text);
+ this.explanation = response.translated_text;
+
+ // 1. Update de Vue instance
+ if (window.__vueApp && window.__vueApp._instance) {
+ window.__vueApp._instance.proxy.explanation = response.translated_text;
+ }
+
+ // 2. Update direct het DOM-element via marked voor onmiddellijke weergave
+ const sidebarElement = document.querySelector('.sidebar-explanation');
+ if (sidebarElement) {
+ console.log('DOM-element gevonden, directe update toepassen');
+ // Gebruik de marked library om de markdown naar HTML te converteren
+ let htmlContent;
+ if (typeof marked === 'function') {
+ htmlContent = marked(response.translated_text);
+ } else if (marked && typeof marked.parse === 'function') {
+ htmlContent = marked.parse(response.translated_text);
+ } else {
+ htmlContent = response.translated_text;
+ }
+
+ // Update de inhoud direct
+ sidebarElement.innerHTML = htmlContent;
+ } else {
+ console.error('Sidebar explanation element niet gevonden in DOM');
+ }
+
+ this.showTranslationIndicator(language, 'Vertaling voltooid!', true);
+ } else {
+ console.error('Vertaling mislukt:', response.error);
+ this.showTranslationIndicator(language, 'Vertaling mislukt', false);
+ }
+ } catch (error) {
+ console.error('Fout bij vertalen sidebar:', error);
+ this.showTranslationIndicator(language, 'Vertaling mislukt', false);
+ }
+ },
+
// Message management
addMessage(content, sender, type = 'text', formData = null, formValues = null) {
const message = {
@@ -206,6 +328,36 @@ export const ChatApp = {
return message;
},
+ showTranslationIndicator(language, message, success = null) {
+ const explanationElement = document.querySelector('.sidebar-explanation');
+ if (explanationElement) {
+ // Verwijder eventuele bestaande indicators
+ const existingIndicator = explanationElement.querySelector('.language-change-indicator');
+ if (existingIndicator) {
+ existingIndicator.remove();
+ }
+
+ // Voeg nieuwe indicator toe
+ const indicator = document.createElement('div');
+ indicator.className = 'language-change-indicator';
+ if (success === true) indicator.classList.add('success');
+ if (success === false) indicator.classList.add('error');
+
+ indicator.innerHTML = `${message}`;
+ explanationElement.prepend(indicator);
+
+ // Verwijder na 3 seconden, behalve bij loading
+ if (success !== null) {
+ setTimeout(() => {
+ if (explanationElement.contains(indicator)) {
+ indicator.remove();
+ }
+ }, 3000);
+ }
+ }
+ },
+
+
// Helper functie om formulierdata toe te voegen aan bestaande berichten
attachFormDataToMessage(messageId, formData, formValues) {
const message = this.allMessages.find(m => m.id === messageId);
@@ -243,7 +395,8 @@ export const ChatApp = {
const apiData = {
message: text,
conversation_id: this.conversationId,
- user_id: this.userId
+ user_id: this.userId,
+ language: this.currentLanguage
};
const response = await this.callAPI('/api/send_message', apiData);
@@ -662,6 +815,7 @@ const initializeApp = () => {
window.__vueApp.component('MessageHistory', MessageHistory);
window.__vueApp.component('ChatInput', ChatInput);
window.__vueApp.component('ProgressTracker', ProgressTracker);
+ // NB: LanguageSelector wordt niet globaal geregistreerd omdat deze apart gemonteerd wordt
console.log('All chat components registered with existing Vue instance');
// Register the ChatApp component
@@ -677,5 +831,63 @@ const initializeApp = () => {
}
};
+// Functie om LanguageSelector toe te voegen aan sidebar
+const mountLanguageSelector = () => {
+ const container = document.getElementById('language-selector-container');
+ if (container) {
+ // Maak een eenvoudige Vue app die alleen de LanguageSelector component mount
+ const app = Vue.createApp({
+ components: { LanguageSelector },
+ data() {
+ return {
+ currentLanguage: window.chatConfig?.language || 'nl',
+ supportedLanguageDetails: window.chatConfig?.supportedLanguageDetails || {},
+ allowedLanguages: window.chatConfig?.allowedLanguages || ['nl', 'en', 'fr', 'de']
+ };
+ },
+ methods: {
+ handleLanguageChange(newLanguage) {
+ console.log(`LanguageSelector: Taal gewijzigd naar ${newLanguage}`);
+
+ // Gebruik ALLEEN de custom event benadering
+ const event = new CustomEvent('language-changed', {
+ detail: { language: newLanguage }
+ });
+ document.dispatchEvent(event);
+ }
+ },
+ template: `
+