- Translations completed for Front-End, Configs (e.g. Forms) and free text.

- Allowed_languages and default_language now part of Tenant Make iso Tenant
- Introduction of Translation into Traicie Selection Specialist
This commit is contained in:
Josako
2025-06-30 14:20:17 +02:00
parent 4338f09f5c
commit fbc9f44ac8
34 changed files with 1206 additions and 220 deletions

View File

@@ -1,6 +0,0 @@
<!-- chat.html -->
{% extends "base.html" %}
{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
{% block head %}

View File

@@ -26,9 +26,6 @@ class Tenant(db.Model):
timezone = db.Column(db.String(50), nullable=True, default='UTC') timezone = db.Column(db.String(50), nullable=True, default='UTC')
type = db.Column(db.String(20), nullable=True, server_default='Active') type = db.Column(db.String(20), nullable=True, server_default='Active')
# language information
default_language = db.Column(db.String(2), nullable=True)
# Entitlements # Entitlements
currency = db.Column(db.String(20), nullable=True) currency = db.Column(db.String(20), nullable=True)
storage_dirty = db.Column(db.Boolean, nullable=True, default=False) storage_dirty = db.Column(db.Boolean, nullable=True, default=False)
@@ -61,7 +58,6 @@ class Tenant(db.Model):
'website': self.website, 'website': self.website,
'timezone': self.timezone, 'timezone': self.timezone,
'type': self.type, 'type': self.type,
'default_language': self.default_language,
'currency': self.currency, 'currency': self.currency,
'default_tenant_make_id': self.default_tenant_make_id, '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) active = db.Column(db.Boolean, nullable=False, default=True)
website = db.Column(db.String(255), nullable=True) website = db.Column(db.String(255), nullable=True)
logo_url = 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) allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
# Chat customisation options # Chat customisation options
@@ -209,6 +206,8 @@ class TenantMake(db.Model):
'website': self.website, 'website': self.website,
'logo_url': self.logo_url, 'logo_url': self.logo_url,
'chat_customisation_options': self.chat_customisation_options, '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) cache_key = db.Column(db.String(16), primary_key=True)
source_text = db.Column(db.Text, nullable=False) source_text = db.Column(db.Text, nullable=False)
translated_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) target_language = db.Column(db.String(2), nullable=False)
context = db.Column(db.Text, nullable=True) context = db.Column(db.Text, nullable=True)

View File

@@ -1,43 +1,108 @@
import xxhash
import json 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 class TranslationServices:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from common.langchain.persistent_llm_metrics_handler import PersistentLLMMetricsHandler @staticmethod
from common.utils.model_utils import get_template, replace_variable_in_template 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: Args:
def __init__(self, tenant_id): config_data: Een dictionary of JSON (die dan wordt geconverteerd naar een dictionary) met configuratiegegevens
self.tenant_id = tenant_id 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[ Returns:
str, dict[str, int | float]]: Een dictionary met de vertaalde configuratie
prompt_params = { """
"text_to_translate": text_to_translate, # Zorg ervoor dat we een dictionary hebben
"target_lang": target_lang, if isinstance(config_data, str):
} config_data = json.loads(config_data)
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 # Maak een kopie van de originele data om te wijzigen
translated_config = config_data.copy()
metrics_handler = PersistentLLMMetricsHandler() # Haal type en versie op voor de Business Event span
existing_callbacks = llm.callbacks config_type = config_data.get('type', 'Unknown')
llm.callbacks = existing_callbacks + [metrics_handler] 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 # 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

View File

@@ -42,7 +42,7 @@ def create_cache_regions(app):
# Region for model-related caching (ModelVariables etc) # Region for model-related caching (ModelVariables etc)
model_region = make_region(name='eveai_model').configure( model_region = make_region(name='eveai_model').configure(
'dogpile.cache.redis', 'dogpile.cache.redis',
arguments=redis_config, arguments={**redis_config, 'db': 6},
replace_existing_backend=True replace_existing_backend=True
) )
regions['eveai_model'] = model_region regions['eveai_model'] = model_region

View File

@@ -1,19 +1,25 @@
import json import json
import re
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
import xxhash import xxhash
from flask import current_app 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 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.utils.cache.base import CacheHandler, T
from common.extensions import db from common.extensions import db
from common.models.user import TranslationCache from common.models.user import TranslationCache
from common.services.utils.translation_services import TranslationService
from flask_security import current_user from flask_security import current_user
from common.utils.model_utils import get_template
class TranslationCacheHandler(CacheHandler[TranslationCache]): class TranslationCacheHandler(CacheHandler[TranslationCache]):
"""Handles caching of translations with fallback to database and external translation service""" """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) 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 return translation
def _should_cache(self, value: TranslationCache) -> bool: def _should_cache(self, value) -> bool:
"""Validate if the translation should be cached""" """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 Get the translation for a text in a specific language
@@ -81,26 +107,33 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
Returns: Returns:
TranslationCache instance if found, None otherwise 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]: def creator_func(hash_key: str) -> Optional[TranslationCache]:
# Generate cache key based on inputs
cache_key = self._generate_cache_key(text, target_lang, source_lang, context)
# Check if translation already exists in database # 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: if existing_translation:
# Update last used timestamp # Update last used timestamp
existing_translation.last_used_at = dt.now(tz=tz.utc) 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() db.session.commit()
return existing_translation return existing_translation
# Translation not found in DB, need to create it # 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 # Get the translation and metrics
translated_text, metrics = translation_service.translate_text( translated_text, metrics = self.translate_text(
text_to_translate=text, text_to_translate=text,
target_lang=target_lang, target_lang=target_lang,
source_lang=source_lang, source_lang=source_lang,
@@ -109,10 +142,10 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
# Create new translation cache record # Create new translation cache record
new_translation = TranslationCache( new_translation = TranslationCache(
cache_key=cache_key, cache_key=hash_key,
source_text=text, source_text=text,
translated_text=translated_text, translated_text=translated_text,
source_language=source_lang or 'auto', source_language=source_lang,
target_language=target_lang, target_language=target_lang,
context=context, context=context,
prompt_tokens=metrics.get('prompt_tokens', 0), prompt_tokens=metrics.get('prompt_tokens', 0),
@@ -130,7 +163,12 @@ class TranslationCacheHandler(CacheHandler[TranslationCache]):
return new_translation 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): def invalidate_tenant_translations(self, tenant_id: int):
"""Invalidate cached translations for specific tenant""" """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) cache_string = json.dumps(cache_data, sort_keys=True, ensure_ascii=False)
return xxhash.xxh64(cache_string.encode('utf-8')).hexdigest() 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: def register_translation_cache_handlers(cache_manager) -> None:
"""Register translation cache handlers with cache manager""" """Register translation cache handlers with cache manager"""
cache_manager.register_handler( cache_manager.register_handler(

View File

@@ -31,7 +31,6 @@ def get_default_chat_customisation(tenant_customisation=None):
'markdown_background_color': 'transparent', 'markdown_background_color': 'transparent',
'markdown_text_color': '#ffffff', 'markdown_text_color': '#ffffff',
'sidebar_markdown': '', 'sidebar_markdown': '',
'welcome_message': 'Hello! How can I help you today?',
} }
# If no tenant customization is provided, return the defaults # If no tenant customization is provided, return the defaults

View File

@@ -12,7 +12,6 @@ from datetime import datetime as dt, timezone as tz
def set_tenant_session_data(sender, user, **kwargs): def set_tenant_session_data(sender, user, **kwargs):
tenant = Tenant.query.filter_by(id=user.tenant_id).first() tenant = Tenant.query.filter_by(id=user.tenant_id).first()
session['tenant'] = tenant.to_dict() session['tenant'] = tenant.to_dict()
session['default_language'] = tenant.default_language
partner = Partner.query.filter_by(tenant_id=user.tenant_id).first() partner = Partner.query.filter_by(tenant_id=user.tenant_id).first()
if partner: if partner:
session['partner'] = partner.to_dict() session['partner'] = partner.to_dict()

View File

@@ -66,7 +66,6 @@ class Config(object):
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 MAX_CONTENT_LENGTH = 50 * 1024 * 1024
# supported languages # supported languages
SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi']
SUPPORTED_LANGUAGE_DETAILS = { SUPPORTED_LANGUAGE_DETAILS = {
"English": { "English": {
"iso 639-1": "en", "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_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
SUPPORTED_CURRENCIES = ['', '$'] SUPPORTED_CURRENCIES = ['', '$']
@@ -293,6 +295,8 @@ class DevConfig(Config):
CHAT_WORKER_CACHE_URL = f'{REDIS_BASE_URI}/4' CHAT_WORKER_CACHE_URL = f'{REDIS_BASE_URI}/4'
# specialist execution pub/sub Redis Settings # specialist execution pub/sub Redis Settings
SPECIALIST_EXEC_PUBSUB = f'{REDIS_BASE_URI}/5' SPECIALIST_EXEC_PUBSUB = f'{REDIS_BASE_URI}/5'
# eveai_model cache Redis setting
MODEL_CACHE_URL = f'{REDIS_BASE_URI}/6'
# Unstructured settings # Unstructured settings

View File

@@ -1,9 +1,16 @@
version: "1.0.0" version: "1.0.0"
content: > content: >
You are a top translator. We need you to translate {text_to_translate} into {target_language}, taking into account You are a top translator. We need you to translate (in between triple quotes)
this context:
{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 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. without further interpretation. If more than one option is available, present me with the most probable one.

View File

@@ -1,6 +1,13 @@
version: "1.0.0" version: "1.0.0"
content: > 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 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. without further interpretation. If more than one option is available, present me with the most probable one.

View File

@@ -8,6 +8,7 @@ fields:
description: "Your name" description: "Your name"
type: "str" type: "str"
required: true required: true
# It is possible to also add a field 'context'. It allows you to provide an elaborate piece of information.
email: email:
name: "Email" name: "Email"
type: "str" type: "str"
@@ -17,7 +18,6 @@ fields:
name: "Phone Number" name: "Phone Number"
type: "str" type: "str"
description: "Your Phone Number" description: "Your Phone Number"
context: "Een kleine test om te zien of we context kunnen doorgeven en tonen"
required: true required: true
address: address:
name: "Address" name: "Address"
@@ -44,3 +44,8 @@ fields:
type: "boolean" type: "boolean"
description: "Consent" description: "Consent"
required: true required: true
metadata:
author: "Josako"
date_added: "2025-06-18"
changes: "Initial Version"
description: "Personal Contact Form"

View File

@@ -53,3 +53,8 @@ fields:
type: "bool" type: "bool"
description: "Consent" description: "Consent"
required: true required: true
metadata:
author: "Josako"
date_added: "2025-06-18"
changes: "Initial Version"
description: "Professional Contact Form"

View File

@@ -106,20 +106,20 @@ def create_app(config_file=None):
from flask_login import current_user from flask_login import current_user
import datetime import datetime
app.logger.debug(f"Before request - URL: {request.url}") # 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 - Session permanent: {session.permanent}")
# Log session expiry tijd als deze bestaat # Log session expiry tijd als deze bestaat
if current_user.is_authenticated: if current_user.is_authenticated:
# Controleer of sessie permanent is (nodig voor PERMANENT_SESSION_LIFETIME) # Controleer of sessie permanent is (nodig voor PERMANENT_SESSION_LIFETIME)
if not session.permanent: if not session.permanent:
session.permanent = True 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 # Log wanneer sessie zou verlopen
if '_permanent' in session: # if '_permanent' in session:
expires_at = datetime.datetime.now() + app.permanent_session_lifetime # expires_at = datetime.datetime.now() + app.permanent_session_lifetime
app.logger.debug(f"Session will expire at: {expires_at} (60 min from now)") # app.logger.debug(f"Session will expire at: {expires_at} (60 min from now)")
@app.route('/debug/session') @app.route('/debug/session')
def debug_session(): def debug_session():

View File

@@ -16,7 +16,6 @@ class SessionDefaultsForm(FlaskForm):
# Tenant Defaults # Tenant Defaults
tenant_name = StringField('Tenant Name', validators=[DataRequired()]) tenant_name = StringField('Tenant Name', validators=[DataRequired()])
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# Partner Defaults # Partner Defaults
partner_name = StringField('Partner Name', validators=[DataRequired()]) partner_name = StringField('Partner Name', validators=[DataRequired()])

View File

@@ -59,7 +59,6 @@ def session_defaults():
form = SessionDefaultsForm() form = SessionDefaultsForm()
if form.validate_on_submit(): if form.validate_on_submit():
session['default_language'] = form.default_language.data
if form.catalog.data: if form.catalog.data:
catalog_id = int(form.catalog.data) catalog_id = int(form.catalog.data)
catalog = tenant_session.query(Catalog).get(catalog_id) catalog = tenant_session.query(Catalog).get(catalog_id)

View File

@@ -453,9 +453,6 @@ class DynamicFormBase(FlaskForm):
else: else:
render_kw['class'] = 'color-field' render_kw['class'] = 'color-field'
current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
# Create the field # Create the field
field_kwargs.update({ field_kwargs.update({
'label': label, 'label': label,

View File

@@ -18,8 +18,6 @@ class TenantForm(FlaskForm):
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True}) code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active') type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# invoicing fields # invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()]) currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone # Timezone
@@ -32,8 +30,6 @@ class TenantForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TenantForm, self).__init__(*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 # initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone # initialise timezone
@@ -53,8 +49,6 @@ class EditTenantForm(FlaskForm):
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True}) code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active') type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# invoicing fields # invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()]) currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone # Timezone
@@ -69,8 +63,6 @@ class EditTenantForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditTenantForm, self).__init__(*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 # initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone # initialise timezone
@@ -212,14 +204,17 @@ class EditTenantMakeForm(DynamicFormBase):
active = BooleanField('Active', validators=[Optional()], default=True) active = BooleanField('Active', validators=[Optional()], default=True)
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])
logo_url = StringField('Logo URL', validators=[Optional(), 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()]) allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[Optional()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditTenantMakeForm, self).__init__(*args, **kwargs) super(EditTenantMakeForm, self).__init__(*args, **kwargs)
# Initialiseer de taalopties met taalcodes en vlaggen # Initialiseer de taalopties met taalcodes en vlaggen
lang_details = current_app.config['SUPPORTED_LANGUAGE_DETAILS'] lang_details = current_app.config['SUPPORTED_LANGUAGE_DETAILS']
self.allowed_languages.choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}") choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}")
for name, details in lang_details.items()] for name, details in lang_details.items()]
self.allowed_languages.choices = choices
self.default_language.choices = choices

View File

@@ -309,7 +309,6 @@ def handle_tenant_selection():
# set tenant information in the session # set tenant information in the session
session['tenant'] = the_tenant.to_dict() session['tenant'] = the_tenant.to_dict()
session['default_language'] = the_tenant.default_language
# remove catalog-related items from the session # remove catalog-related items from the session
session.pop('catalog_id', None) session.pop('catalog_id', None)
session.pop('catalog_name', None) session.pop('catalog_name', None)
@@ -706,6 +705,7 @@ def edit_tenant_make(tenant_make_id):
form = EditTenantMakeForm(request.form, obj=tenant_make) form = EditTenantMakeForm(request.form, obj=tenant_make)
# Initialiseer de allowed_languages selectie met huidige waarden # Initialiseer de allowed_languages selectie met huidige waarden
if request.method == 'GET':
if tenant_make.allowed_languages: if tenant_make.allowed_languages:
form.allowed_languages.data = tenant_make.allowed_languages form.allowed_languages.data = tenant_make.allowed_languages
@@ -717,7 +717,9 @@ def edit_tenant_make(tenant_make_id):
form.populate_obj(tenant_make) form.populate_obj(tenant_make)
tenant_make.chat_customisation_options = form.get_dynamic_data("configuration") tenant_make.chat_customisation_options = form.get_dynamic_data("configuration")
# Verwerk allowed_languages als array # 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 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
update_logging_information(tenant_make, dt.now(tz.utc)) update_logging_information(tenant_make, dt.now(tz.utc))

View File

@@ -112,3 +112,5 @@ def register_cache_handlers(app):
register_config_cache_handlers(cache_manager) register_config_cache_handlers(cache_manager)
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
register_specialist_cache_handlers(cache_manager) register_specialist_cache_handlers(cache_manager)
from common.utils.cache.translation_cache import register_translation_cache_handlers
register_translation_cache_handlers(cache_manager)

View File

@@ -11,10 +11,85 @@
--spacing: 16px; --spacing: 16px;
} }
* { /* App container layout */
box-sizing: border-box; .app-container {
margin: 0; display: flex;
padding: 0; 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 { body {
@@ -35,11 +110,14 @@ body {
.chat-container { .chat-container {
display: flex; display: flex;
height: 100%; height: 100%;
flex: 1;
flex-direction: column;
min-height: 0;
} }
.sidebar { .sidebar {
width: 280px; width: 280px;
background-color: var(--sidebar-color); background-color: var(--sidebar-background);
border-right: 1px solid rgba(0,0,0,0.1); border-right: 1px solid rgba(0,0,0,0.1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -98,6 +176,35 @@ body {
border-bottom: 1px solid rgba(0,0,0,0.1); 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 */ /* .chat-messages wordt nu gedefinieerd in chat-components.css */
/* .message wordt nu gedefinieerd in chat-components.css */ /* .message wordt nu gedefinieerd in chat-components.css */

View File

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

View File

@@ -5,6 +5,7 @@ import { DynamicForm } from '/static/assets/js/components/DynamicForm.js';
import { ChatMessage } from '/static/assets/js/components/ChatMessage.js'; import { ChatMessage } from '/static/assets/js/components/ChatMessage.js';
import { MessageHistory } from '/static/assets/js/components/MessageHistory.js'; import { MessageHistory } from '/static/assets/js/components/MessageHistory.js';
import { ProgressTracker } from '/static/assets/js/components/ProgressTracker.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 // Maak componenten globaal beschikbaar voordat andere componenten worden geladen
window.DynamicForm = DynamicForm; window.DynamicForm = DynamicForm;
@@ -33,9 +34,15 @@ export const ChatApp = {
// Maak een lokale kopie van de chatConfig om undefined errors te voorkomen // Maak een lokale kopie van de chatConfig om undefined errors te voorkomen
const chatConfig = window.chatConfig || {}; const chatConfig = window.chatConfig || {};
const settings = chatConfig.settings || {}; const settings = chatConfig.settings || {};
const initialLanguage = chatConfig.language || 'nl';
const originalExplanation = chatConfig.explanation || '';
return { 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 || '', explanation: chatConfig.explanation || '',
// Chat-specific data // Chat-specific data
@@ -164,6 +171,14 @@ export const ChatApp = {
// Keyboard shortcuts // Keyboard shortcuts
document.addEventListener('keydown', this.handleGlobalKeydown); 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() { cleanup() {
@@ -171,6 +186,113 @@ export const ChatApp = {
document.removeEventListener('keydown', this.handleGlobalKeydown); 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 // Message management
addMessage(content, sender, type = 'text', formData = null, formValues = null) { addMessage(content, sender, type = 'text', formData = null, formValues = null) {
const message = { const message = {
@@ -206,6 +328,36 @@ export const ChatApp = {
return message; 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 = `<small>${message}</small>`;
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 // Helper functie om formulierdata toe te voegen aan bestaande berichten
attachFormDataToMessage(messageId, formData, formValues) { attachFormDataToMessage(messageId, formData, formValues) {
const message = this.allMessages.find(m => m.id === messageId); const message = this.allMessages.find(m => m.id === messageId);
@@ -243,7 +395,8 @@ export const ChatApp = {
const apiData = { const apiData = {
message: text, message: text,
conversation_id: this.conversationId, conversation_id: this.conversationId,
user_id: this.userId user_id: this.userId,
language: this.currentLanguage
}; };
const response = await this.callAPI('/api/send_message', apiData); const response = await this.callAPI('/api/send_message', apiData);
@@ -662,6 +815,7 @@ const initializeApp = () => {
window.__vueApp.component('MessageHistory', MessageHistory); window.__vueApp.component('MessageHistory', MessageHistory);
window.__vueApp.component('ChatInput', ChatInput); window.__vueApp.component('ChatInput', ChatInput);
window.__vueApp.component('ProgressTracker', ProgressTracker); 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'); console.log('All chat components registered with existing Vue instance');
// Register the ChatApp component // 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: `
<language-selector
:initial-language="currentLanguage"
:supported-language-details="supportedLanguageDetails"
:allowed-languages="allowedLanguages"
@language-changed="handleLanguageChange"
></language-selector>
`
});
app.component('LanguageSelector', LanguageSelector);
app.mount('#language-selector-container');
console.log('Language selector mounted in sidebar');
} else {
console.warn('Language selector container not found');
}
};
// Initialize app when DOM is ready // Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', initializeApp); document.addEventListener('DOMContentLoaded', () => {
console.log('DOM content loaded, initializing application...');
// Eerst de hoofdapplicatie initialiseren
initializeApp();
// Dan de taal selector monteren met een kleine vertraging
// om er zeker van te zijn dat de container is aangemaakt
setTimeout(() => {
try {
mountLanguageSelector();
} catch (e) {
console.error('Fout bij het monteren van de taal selector:', e);
}
}, 200);
});

View File

@@ -29,6 +29,22 @@
if (window.iconManager && this.formData && this.formData.icon) { if (window.iconManager && this.formData && this.formData.icon) {
window.iconManager.ensureIconsLoaded({}, [this.formData.icon]); window.iconManager.ensureIconsLoaded({}, [this.formData.icon]);
} }
// Maak een benoemde handler voor betere cleanup
this.languageChangeHandler = (event) => {
if (event.detail && event.detail.language) {
this.handleLanguageChange(event);
}
};
// Luister naar taalwijzigingen
document.addEventListener('language-changed', this.languageChangeHandler);
},
beforeUnmount() {
// Verwijder event listener bij unmount met de benoemde handler
if (this.languageChangeHandler) {
document.removeEventListener('language-changed', this.languageChangeHandler);
}
}, },
props: { props: {
currentMessage: { currentMessage: {
@@ -41,7 +57,7 @@
}, },
placeholder: { placeholder: {
type: String, type: String,
default: 'Typ je bericht hier... (Enter om te verzenden, Shift+Enter voor nieuwe regel)' default: 'Typ je bericht hier... - Enter om te verzenden, Shift+Enter voor nieuwe regel'
}, },
maxLength: { maxLength: {
type: Number, type: Number,
@@ -103,7 +119,10 @@
data() { data() {
return { return {
localMessage: this.currentMessage, localMessage: this.currentMessage,
formValues: {} formValues: {},
translatedPlaceholder: this.placeholder,
isTranslating: false,
languageChangeHandler: null
}; };
}, },
computed: { computed: {
@@ -150,6 +169,56 @@
} }
}, },
methods: { methods: {
handleLanguageChange(event) {
if (event.detail && event.detail.language) {
this.translatePlaceholder(event.detail.language);
}
},
async translatePlaceholder(language) {
// Voorkom dubbele vertaling
if (this.isTranslating) {
console.log('Placeholder vertaling al bezig, overslaan...');
return;
}
// Zet de vertaling vlag
this.isTranslating = true;
// Gebruik de originele placeholder als basis voor vertaling
const originalText = this.placeholder;
try {
// Controleer of TranslationClient beschikbaar is
if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
console.error('TranslationClient.translate is niet beschikbaar voor placeholder');
return;
}
// Gebruik TranslationClient zonder UI indicator
const apiPrefix = window.chatConfig?.apiPrefix || '';
const response = await window.TranslationClient.translate(
originalText,
language,
null, // source_lang (auto-detect)
'chat_input_placeholder', // context
apiPrefix // API prefix voor tenant routing
);
if (response.success) {
// Update de placeholder
this.translatedPlaceholder = response.translated_text;
} else {
console.error('Vertaling placeholder mislukt:', response.error);
}
} catch (error) {
console.error('Fout bij vertalen placeholder:', error);
} finally {
// Reset de vertaling vlag
this.isTranslating = false;
}
},
initFormValues() { initFormValues() {
if (this.formData && this.formData.fields) { if (this.formData && this.formData.fields) {
console.log('Initializing form values for fields:', this.formData.fields); console.log('Initializing form values for fields:', this.formData.fields);
@@ -300,7 +369,7 @@
ref="messageInput" ref="messageInput"
v-model="localMessage" v-model="localMessage"
@keydown="handleKeydown" @keydown="handleKeydown"
:placeholder="placeholder" :placeholder="translatedPlaceholder"
rows="1" rows="1"
:disabled="isLoading" :disabled="isLoading"
:maxlength="maxLength" :maxlength="maxLength"

View File

@@ -52,6 +52,11 @@ export const ChatMessage = {
if (window.iconManager && this.message.formData && this.message.formData.icon) { if (window.iconManager && this.message.formData && this.message.formData.icon) {
window.iconManager.loadIcon(this.message.formData.icon); window.iconManager.loadIcon(this.message.formData.icon);
} }
// Sla de originele inhoud op voor het eerste bericht als we in een conversatie zitten met slechts één bericht
if (this.message.sender === 'ai' && !this.message.originalContent) {
this.message.originalContent = this.message.content;
}
}, },
watch: { watch: {
'message.formData.icon': { 'message.formData.icon': {
@@ -69,6 +74,14 @@ export const ChatMessage = {
formVisible: true formVisible: true
}; };
}, },
mounted() {
// Luister naar taalwijzigingen
document.addEventListener('language-changed', this.handleLanguageChange);
},
beforeUnmount() {
// Verwijder event listener bij verwijderen component
document.removeEventListener('language-changed', this.handleLanguageChange);
},
computed: { computed: {
hasFormData() { hasFormData() {
return this.message.formData && return this.message.formData &&
@@ -80,6 +93,12 @@ export const ChatMessage = {
} }
}, },
methods: { methods: {
async handleLanguageChange(event) {
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
// Dit wordt al afgehandeld door MessageHistory component, dus we hoeven hier niets te doen
// De implementatie hiervan blijft in MessageHistory om dubbele vertaling te voorkomen
},
handleSpecialistError(eventData) { handleSpecialistError(eventData) {
console.log('ChatMessage received specialist-error event:', eventData); console.log('ChatMessage received specialist-error event:', eventData);

View File

@@ -0,0 +1,118 @@
export const LanguageSelector = {
name: 'LanguageSelector',
props: {
initialLanguage: {
type: String,
default: 'nl'
},
supportedLanguageDetails: {
type: Object,
default: () => ({})
},
allowedLanguages: {
type: Array,
default: () => ['nl', 'en', 'fr', 'de']
}
},
computed: {
availableLanguages() {
// Maak een array van taalobjecten op basis van de toegestane talen
// en de ondersteunde taaldetails
const languages = [];
// Als er geen toegestane talen zijn of de lijst is leeg, gebruik een standaardlijst
const languagesToUse = (this.allowedLanguages && this.allowedLanguages.length > 0)
? this.allowedLanguages
: ['nl', 'en', 'fr', 'de'];
// Loop door alle ondersteunde taaldetails
if (this.supportedLanguageDetails && Object.keys(this.supportedLanguageDetails).length > 0) {
// Vind talen die overeenkomen met toegestane taalcodes
for (const [langName, langDetails] of Object.entries(this.supportedLanguageDetails)) {
const isoCode = langDetails['iso 639-1'];
if (languagesToUse.includes(isoCode)) {
languages.push({
code: isoCode,
name: langName,
flag: langDetails.flag || ''
});
}
}
} else {
// Fallback als er geen taaldetails beschikbaar zijn
const defaultLanguages = {
'nl': { name: 'Nederlands', flag: '🇳🇱' },
'en': { name: 'English', flag: '🇬🇧' },
'fr': { name: 'Français', flag: '🇫🇷' },
'de': { name: 'Deutsch', flag: '🇩🇪' }
};
languagesToUse.forEach(code => {
if (defaultLanguages[code]) {
languages.push({
code: code,
name: defaultLanguages[code].code,
flag: defaultLanguages[code].flag
});
}
});
}
console.log('LanguageSelector: availableLanguages computed:', languages);
return languages;
}
},
emits: ['language-changed'],
data() {
return {
selectedLanguage: this.initialLanguage,
currentLanguage: this.initialLanguage
};
},
mounted() {
console.log('LanguageSelector mounted with:', {
initialLanguage: this.initialLanguage,
selectedLanguage: this.selectedLanguage,
availableLanguages: this.availableLanguages
});
// Stuur het language-changed event uit met de initiële taal
console.log(`LanguageSelector: Emitting language-changed event for initial language ${this.initialLanguage}`);
this.$emit('language-changed', this.initialLanguage);
},
methods: {
changeLanguage(languageCode) {
console.log(`LanguageSelector: changeLanguage called with ${languageCode}, current: ${this.currentLanguage}`);
if (this.currentLanguage !== languageCode) {
console.log(`LanguageSelector: Emitting language-changed event for ${languageCode}`);
this.currentLanguage = languageCode;
this.$emit('language-changed', languageCode);
} else {
console.log(`LanguageSelector: No change needed, already ${languageCode}`);
}
}
},
template: `
<div class="language-selector">
<label for="language-select">Taal / Language:</label>
<div class="select-wrapper">
<select
id="language-select"
v-model="selectedLanguage"
@change="changeLanguage(selectedLanguage); currentLanguage = selectedLanguage"
class="language-select"
>
<option
v-for="lang in availableLanguages"
:key="lang.code"
:value="lang.code"
>
{{ lang.flag }} {{ lang.name }}
</option>
</select>
</div>
</div>
`
};

View File

@@ -27,12 +27,21 @@ export const MessageHistory = {
data() { data() {
return { return {
isAtBottom: true, isAtBottom: true,
unreadCount: 0 unreadCount: 0,
originalFirstMessage: null,
isTranslating: false, // Vlag om dubbele vertaling te voorkomen
languageChangeHandler: null // Referentie voor cleanup
}; };
}, },
mounted() { mounted() {
this.scrollToBottom(); this.scrollToBottom();
this.setupScrollListener(); this.setupScrollListener();
this.listenForLanguageChanges();
// Sla de originele inhoud van het eerste bericht op als er maar één bericht is
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
this.originalFirstMessage = this.messages[0].content;
}
}, },
updated() { updated() {
if (this.autoScroll && this.isAtBottom) { if (this.autoScroll && this.isAtBottom) {
@@ -40,6 +49,73 @@ export const MessageHistory = {
} }
}, },
methods: { methods: {
listenForLanguageChanges() {
// Maak een benoemde handler voor cleanup
this.languageChangeHandler = (event) => {
if (event.detail && event.detail.language) {
this.translateFirstMessageIfNeeded(event.detail.language);
}
};
document.addEventListener('language-changed', this.languageChangeHandler);
},
async translateFirstMessageIfNeeded(language) {
// Voorkom dubbele vertaling
if (this.isTranslating) {
console.log('Vertaling al bezig, overslaan...');
return;
}
// Alleen vertalen als er precies één bericht is en het is van de AI
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
const firstMessage = this.messages[0];
// Controleer of we een origineel bericht hebben om te vertalen
const contentToTranslate = this.originalFirstMessage || firstMessage.content;
// Sla het originele bericht op als we dat nog niet hebben gedaan
if (!this.originalFirstMessage) {
this.originalFirstMessage = contentToTranslate;
}
// Zet de vertaling vlag
this.isTranslating = true;
try {
// Controleer of de vertaalclient beschikbaar is
if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
console.error('TranslationClient.translate is niet beschikbaar');
return;
}
console.log(`Vertalen van eerste bericht naar ${language}`);
// Vertaal het bericht met de juiste context
const response = await window.TranslationClient.translate(
contentToTranslate,
language,
null, // source_lang (auto-detect)
'first_message', // context
this.apiPrefix // API prefix voor tenant routing
);
if (response.success) {
console.log('Vertaling van eerste bericht voltooid:', response.translated_text);
// Update het bericht zonder een indicator te tonen
firstMessage.content = response.translated_text;
} else {
console.error('Vertaling van eerste bericht mislukt:', response.error);
}
} catch (error) {
console.error('Fout bij het vertalen van eerste bericht:', error);
} finally {
// Reset de vertaling vlag
this.isTranslating = false;
}
}
},
scrollToBottom() { scrollToBottom() {
const container = this.$refs.messagesContainer; const container = this.$refs.messagesContainer;
if (container) { if (container) {
@@ -98,6 +174,11 @@ export const MessageHistory = {
if (container) { if (container) {
container.removeEventListener('scroll', this.handleScroll); container.removeEventListener('scroll', this.handleScroll);
} }
// Cleanup language change listener
if (this.languageChangeHandler) {
document.removeEventListener('language-changed', this.languageChangeHandler);
}
}, },
template: ` template: `
<div class="message-history-container"> <div class="message-history-container">

View File

@@ -0,0 +1,63 @@
/**
* EveAI Vertaal API Client
* Functies voor het vertalen van tekst via de EveAI API
*/
const TranslationClient = {
/**
* Vertaalt een tekst naar de opgegeven doeltaal
*
* @param {string} text - De te vertalen tekst
* @param {string} targetLang - ISO 639-1 taalcode van de doeltaal
* @param {string|null} sourceLang - (Optioneel) ISO 639-1 taalcode van de brontaal
* @param {string|null} context - (Optioneel) Context voor de vertaling
* @param {string|null} apiPrefix - (Optioneel) API prefix voor tenant routing
* @returns {Promise<object>} - Een promise met het vertaalresultaat
*/
translate: async function(text, targetLang, sourceLang = null, context = null, apiPrefix = '') {
try {
// Voorbereiding van de aanvraagdata
const requestData = {
text: text,
target_lang: targetLang
};
// Voeg optionele parameters toe indien aanwezig
if (sourceLang) requestData.source_lang = sourceLang;
if (context) requestData.context = context;
// Bouw de juiste endpoint URL met prefix
const endpoint = `${apiPrefix}/chat/api/translate`;
console.log(`Vertaling aanvragen op endpoint: ${endpoint}`);
// Doe het API-verzoek
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
// Controleer of het verzoek succesvol was
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Onbekende fout bij vertalen');
}
// Verwerk het resultaat
return await response.json();
} catch (error) {
console.error('Vertaalfout:', error);
throw error;
}
}
};
// Maak TranslationClient globaal beschikbaar
window.TranslationClient = TranslationClient;
// Geen auto-initialisatie meer nodig
// De Vue-based LanguageSelector component neemt deze taak over
console.log('TranslationClient geladen en klaar voor gebruik');

View File

@@ -7,13 +7,17 @@
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/chat.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='assets/css/chat.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/language-selector.css') }}">
<!-- Vue.js --> <!-- Vue.js (productie versie) -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- Markdown parser for explanation text --> <!-- Markdown parser for explanation text -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Translation support -->
<script src="{{ url_for('static', filename='assets/js/translation.js') }}"></script>
<!-- Custom theme colors from tenant settings --> <!-- Custom theme colors from tenant settings -->
<style> <style>
:root { :root {
@@ -28,106 +32,12 @@
--markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }}; --markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }};
--markdown-text-color: {{ customisation.markdown_text_color|default('#ffffff') }}; --markdown-text-color: {{ customisation.markdown_text_color|default('#ffffff') }};
} }
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
}
.app-container {
display: flex;
height: 100vh;
width: 100%;
}
.sidebar {
width: 300px;
background-color: var(--sidebar-background);
color: white;
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;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
</style> </style>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<div id="app" class="app-container"> <div id="app" class="app-container" data-vue-app="true">
<!-- Left sidebar - never changes --> <!-- Left sidebar - never changes -->
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-logo"> <div class="sidebar-logo">
@@ -136,6 +46,7 @@
<div class="sidebar-make-name"> <div class="sidebar-make-name">
{{ tenant_make.name|default('') }} {{ tenant_make.name|default('') }}
</div> </div>
<div id="language-selector-container"></div>
<div class="sidebar-explanation" v-html="compiledExplanation"></div> <div class="sidebar-explanation" v-html="compiledExplanation"></div>
</div> </div>
@@ -148,6 +59,19 @@
</div> </div>
<script> <script>
// Maak ondersteunde talen beschikbaar voor de client
window.config = {
supportedLanguages: [
{% for lang_code in config.SUPPORTED_LANGUAGES %}
{
code: "{{ lang_code }}",
name: "{{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['iso 639-1'] }}",
flag: "{{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['flag'] }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
};
// Create Vue app and make it available globally // Create Vue app and make it available globally
window.__vueApp = Vue.createApp({ window.__vueApp = Vue.createApp({
data() { data() {

View File

@@ -21,7 +21,10 @@
autoScroll: {{ settings.auto_scroll|default('true')|lower }}, autoScroll: {{ settings.auto_scroll|default('true')|lower }},
allowReactions: {{ settings.allow_reactions|default('true')|lower }} allowReactions: {{ settings.allow_reactions|default('true')|lower }}
}, },
apiPrefix: '{{ request.headers.get("X-Forwarded-Prefix", "") }}' apiPrefix: '{{ request.headers.get("X-Forwarded-Prefix", "") }}',
language: '{{ session.magic_link.specialist_args.language|default("nl") }}',
supportedLanguageDetails: {{ config.SUPPORTED_LANGUAGE_DETAILS|tojson|safe }},
allowedLanguages: {{ tenant_make.allowed_languages|tojson|safe }}
}; };
// Debug info om te controleren of chatConfig correct is ingesteld // Debug info om te controleren of chatConfig correct is ingesteld

View File

@@ -1,3 +1,4 @@
import json
import uuid import uuid
from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -6,9 +7,12 @@ from common.extensions import db
from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
from common.services.interaction.specialist_services import SpecialistServices from common.services.interaction.specialist_services import SpecialistServices
from common.utils.business_event import BusinessEvent
from common.utils.business_event_context import current_event
from common.utils.database import Database from common.utils.database import Database
from common.utils.chat_utils import get_default_chat_customisation from common.utils.chat_utils import get_default_chat_customisation
from common.utils.execution_progress import ExecutionProgressTracker from common.utils.execution_progress import ExecutionProgressTracker
from common.extensions import cache_manager
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat') chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
@@ -103,13 +107,20 @@ def chat(magic_link_code):
"auto_scroll": True "auto_scroll": True
} }
specialist_config = specialist.configuration
if isinstance(specialist_config, str):
specialist_config = json.loads(specialist_config)
welcome_message = specialist_config.get('welcome_message', 'Hello! How can I help you today?')
return render_template('chat.html', return render_template('chat.html',
tenant=tenant, tenant=tenant,
tenant_make=tenant_make, tenant_make=tenant_make,
specialist=specialist, specialist=specialist,
customisation=customisation, customisation=customisation,
messages=[customisation['welcome_message']], messages=[welcome_message],
settings=settings settings=settings,
config=current_app.config
) )
except Exception as e: except Exception as e:
@@ -149,6 +160,11 @@ def send_message():
if form_values: if form_values:
specialist_args['form_values'] = form_values specialist_args['form_values'] = form_values
# Add language to specialist arguments if present
user_language = data.get('language')
if user_language:
specialist_args['language'] = user_language
current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n" current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n"
f" with args: {specialist_args}\n" f" with args: {specialist_args}\n"
f"with session ID: {chat_session_id}") f"with session ID: {chat_session_id}")
@@ -255,3 +271,78 @@ def task_progress_stream(task_id):
current_app.logger.error(f"Failed to start progress stream: {str(e)}") current_app.logger.error(f"Failed to start progress stream: {str(e)}")
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@chat_bp.route('/api/translate', methods=['POST'])
def translate():
"""
API endpoint om tekst te vertalen naar een doeltaal
Parameters (JSON):
- text: de tekst die moet worden vertaald
- target_lang: de ISO 639-1 taalcode waarnaar moet worden vertaald
- source_lang: (optioneel) de ISO 639-1 taalcode van de brontaal
- context: (optioneel) context voor de vertaling
Returns:
JSON met vertaalde tekst
"""
try:
tenant_id = session.get('tenant', {}).get('id')
with BusinessEvent('Client Translation Service', tenant_id):
with current_event.create_span('Front-End Translation'):
data = request.json
# Valideer vereiste parameters
if not data or 'text' not in data or 'target_lang' not in data:
return jsonify({
'success': False,
'error': 'Required parameters missing: text and/or target_lang'
}), 400
text = data.get('text')
target_lang = data.get('target_lang')
source_lang = data.get('source_lang')
context = data.get('context')
# Controleer of tekst niet leeg is
if not text.strip():
return jsonify({
'success': False,
'error': 'Text to translate cannot be empty'
}), 400
# Haal tenant_id uit sessie
tenant_id = session.get('tenant', {}).get('id')
if not tenant_id:
current_app.logger.error("No tenant ID found in session")
# Fallback naar huidige app tenant_id
tenant_id = getattr(current_app, 'tenant_id', None)
# Haal vertaling op (uit cache of genereer nieuw)
translation = cache_manager.translation_cache.get_translation(
text=text,
target_lang=target_lang,
source_lang=source_lang,
context=context
)
if not translation:
return jsonify({
'success': False,
'error': 'No translation found in cache. Please try again later.'
}), 500
# Retourneer het resultaat
return jsonify({
'success': True,
'translated_text': translation.translated_text,
'source_language': translation.source_language,
'target_language': translation.target_language
})
except Exception as e:
current_app.logger.error(f"Error translating: {str(e)}", exc_info=True)
return jsonify({
'success': False,
'error': f"Error translating: {str(e)}"
}), 500

View File

@@ -52,6 +52,8 @@ def register_cache_handlers(app):
register_specialist_cache_handlers(cache_manager) register_specialist_cache_handlers(cache_manager)
from eveai_chat_workers.chat_session_cache import register_chat_session_cache_handlers from eveai_chat_workers.chat_session_cache import register_chat_session_cache_handlers
register_chat_session_cache_handlers(cache_manager) register_chat_session_cache_handlers(cache_manager)
from common.utils.cache.translation_cache import register_translation_cache_handlers
register_translation_cache_handlers(cache_manager)
app, celery = create_app() app, celery = create_app()

View File

@@ -12,6 +12,7 @@ from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db from common.extensions import db
from common.models.user import Tenant from common.models.user import Tenant
from common.models.interaction import Specialist from common.models.interaction import Specialist
from common.services.utils.translation_services import TranslationServices
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestions, KOQuestion from eveai_chat_workers.outputs.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestions, KOQuestion
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
@@ -177,8 +178,15 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
"fields": fields, "fields": fields,
} }
answer = f"Let's start our selection process by asking you a few important questions."
if arguments.language != 'en':
TranslationServices.translate_config(ko_form, "fields", arguments.language)
TranslationServices.translate(answer, arguments.language)
results = SpecialistResult.create_for_type(self.type, self.type_version, results = SpecialistResult.create_for_type(self.type, self.type_version,
answer=f"We starten met een aantal KO Criteria vragen", answer=answer,
form_request=ko_form, form_request=ko_form,
phase="ko_question_evaluation") phase="ko_question_evaluation")
@@ -208,15 +216,27 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
break break
if evaluation == "negative": if evaluation == "negative":
answer = (f"We hebben de antwoorden op onze eerste vragen verwerkt. Je voldoet jammer genoeg niet aan de "
f"minimale vereisten voor deze job.")
if arguments.language != 'nl':
answer = TranslationServices.translate(answer, arguments.language)
results = SpecialistResult.create_for_type(self.type, self.type_version, results = SpecialistResult.create_for_type(self.type, self.type_version,
answer=f"We hebben de antwoorden op de KO criteria verwerkt. Je voldoet jammer genoeg niet aan de minimale vereisten voor deze job.", answer=answer,
form_request=None, form_request=None,
phase="no_valid_candidate") phase="no_valid_candidate")
else: else:
answer = (f"We hebben de antwoorden op de KO criteria verwerkt. Je bent een geschikte kandidaat. "
f"Ben je bereid je contactgegevens door te geven, zodat we je kunnen contacteren voor een verder "
f"gesprek?")
# Check if answers to questions are positive # Check if answers to questions are positive
contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0") contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0")
if arguments.language != 'nl':
answer = TranslationServices.translate(answer, arguments.language)
if arguments.language != 'en':
contact_form = TranslationServices.translate_config(contact_form, "fields", arguments.language)
results = SpecialistResult.create_for_type(self.type, self.type_version, results = SpecialistResult.create_for_type(self.type, self.type_version,
answer=f"We hebben de antwoorden op de KO criteria verwerkt. Je bent een geschikte kandidaat. Kan je je contactegevens doorgeven?", answer=answer,
form_request=contact_form, form_request=contact_form,
phase="personal_contact_data") phase="personal_contact_data")

View File

@@ -0,0 +1,36 @@
"""Remove not null constraint from source language in TranslationCache
Revision ID: 057fb975f0e3
Revises: bb67d16f428a
Create Date: 2025-06-27 13:09:28.171399
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '057fb975f0e3'
down_revision = 'bb67d16f428a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('translation_cache', schema=None) as batch_op:
batch_op.alter_column('source_language',
existing_type=sa.VARCHAR(length=2),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('translation_cache', schema=None) as batch_op:
batch_op.alter_column('source_language',
existing_type=sa.VARCHAR(length=2),
nullable=False)
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""Move default language to Tenant Make
Revision ID: bb67d16f428a
Revises: e47dc002b678
Create Date: 2025-06-27 06:38:35.092499
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'bb67d16f428a'
down_revision = 'e47dc002b678'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.drop_column('allowed_languages')
batch_op.drop_column('default_language')
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.add_column(sa.Column('default_language', sa.String(length=2), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant_make', schema=None) as batch_op:
batch_op.drop_column('default_language')
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.add_column(sa.Column('default_language', sa.VARCHAR(length=2), autoincrement=False, nullable=True))
batch_op.add_column(sa.Column('allowed_languages', postgresql.ARRAY(sa.VARCHAR(length=2)), autoincrement=False, nullable=True))
# ### end Alembic commands ###