- 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:
@@ -1,6 +0,0 @@
|
||||
<!-- chat.html -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
2
common/utils/cache/regions.py
vendored
2
common/utils/cache/regions.py
vendored
@@ -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
|
||||
|
||||
107
common/utils/cache/translation_cache.py
vendored
107
common/utils/cache/translation_cache.py
vendored
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']}")
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,6 +705,7 @@ def edit_tenant_make(tenant_make_id):
|
||||
form = EditTenantMakeForm(request.form, obj=tenant_make)
|
||||
|
||||
# Initialiseer de allowed_languages selectie met huidige waarden
|
||||
if request.method == 'GET':
|
||||
if 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)
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
50
eveai_chat_client/static/assets/css/language-selector.css
Normal file
50
eveai_chat_client/static/assets/css/language-selector.css
Normal 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;
|
||||
}
|
||||
@@ -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 = `<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
|
||||
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: `
|
||||
<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
|
||||
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);
|
||||
});
|
||||
@@ -29,6 +29,22 @@
|
||||
if (window.iconManager && this.formData && 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: {
|
||||
currentMessage: {
|
||||
@@ -41,7 +57,7 @@
|
||||
},
|
||||
placeholder: {
|
||||
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: {
|
||||
type: Number,
|
||||
@@ -103,7 +119,10 @@
|
||||
data() {
|
||||
return {
|
||||
localMessage: this.currentMessage,
|
||||
formValues: {}
|
||||
formValues: {},
|
||||
translatedPlaceholder: this.placeholder,
|
||||
isTranslating: false,
|
||||
languageChangeHandler: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -150,6 +169,56 @@
|
||||
}
|
||||
},
|
||||
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() {
|
||||
if (this.formData && this.formData.fields) {
|
||||
console.log('Initializing form values for fields:', this.formData.fields);
|
||||
@@ -300,7 +369,7 @@
|
||||
ref="messageInput"
|
||||
v-model="localMessage"
|
||||
@keydown="handleKeydown"
|
||||
:placeholder="placeholder"
|
||||
:placeholder="translatedPlaceholder"
|
||||
rows="1"
|
||||
:disabled="isLoading"
|
||||
:maxlength="maxLength"
|
||||
|
||||
@@ -52,6 +52,11 @@ export const ChatMessage = {
|
||||
if (window.iconManager && this.message.formData && 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: {
|
||||
'message.formData.icon': {
|
||||
@@ -69,6 +74,14 @@ export const ChatMessage = {
|
||||
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: {
|
||||
hasFormData() {
|
||||
return this.message.formData &&
|
||||
@@ -80,6 +93,12 @@ export const ChatMessage = {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
console.log('ChatMessage received specialist-error event:', eventData);
|
||||
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
};
|
||||
@@ -27,12 +27,21 @@ export const MessageHistory = {
|
||||
data() {
|
||||
return {
|
||||
isAtBottom: true,
|
||||
unreadCount: 0
|
||||
unreadCount: 0,
|
||||
originalFirstMessage: null,
|
||||
isTranslating: false, // Vlag om dubbele vertaling te voorkomen
|
||||
languageChangeHandler: null // Referentie voor cleanup
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom();
|
||||
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() {
|
||||
if (this.autoScroll && this.isAtBottom) {
|
||||
@@ -40,6 +49,73 @@ export const MessageHistory = {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
@@ -98,6 +174,11 @@ export const MessageHistory = {
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
// Cleanup language change listener
|
||||
if (this.languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message-history-container">
|
||||
|
||||
63
eveai_chat_client/static/assets/js/translation.js
Normal file
63
eveai_chat_client/static/assets/js/translation.js
Normal 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');
|
||||
@@ -7,13 +7,17 @@
|
||||
|
||||
<!-- 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 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<!-- Vue.js (productie versie) -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
|
||||
<!-- Markdown parser for explanation text -->
|
||||
<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 -->
|
||||
<style>
|
||||
:root {
|
||||
@@ -28,106 +32,12 @@
|
||||
--markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }};
|
||||
--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>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app-container">
|
||||
<div id="app" class="app-container" data-vue-app="true">
|
||||
<!-- Left sidebar - never changes -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
@@ -136,6 +46,7 @@
|
||||
<div class="sidebar-make-name">
|
||||
{{ tenant_make.name|default('') }}
|
||||
</div>
|
||||
<div id="language-selector-container"></div>
|
||||
<div class="sidebar-explanation" v-html="compiledExplanation"></div>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +59,19 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
window.__vueApp = Vue.createApp({
|
||||
data() {
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
autoScroll: {{ settings.auto_scroll|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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import uuid
|
||||
from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context
|
||||
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.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
||||
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.chat_utils import get_default_chat_customisation
|
||||
from common.utils.execution_progress import ExecutionProgressTracker
|
||||
from common.extensions import cache_manager
|
||||
|
||||
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
|
||||
|
||||
@@ -103,13 +107,20 @@ def chat(magic_link_code):
|
||||
"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',
|
||||
tenant=tenant,
|
||||
tenant_make=tenant_make,
|
||||
specialist=specialist,
|
||||
customisation=customisation,
|
||||
messages=[customisation['welcome_message']],
|
||||
settings=settings
|
||||
messages=[welcome_message],
|
||||
settings=settings,
|
||||
config=current_app.config
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -149,6 +160,11 @@ def send_message():
|
||||
if 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"
|
||||
f" with args: {specialist_args}\n"
|
||||
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)}")
|
||||
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
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ def register_cache_handlers(app):
|
||||
register_specialist_cache_handlers(cache_manager)
|
||||
from eveai_chat_workers.chat_session_cache import register_chat_session_cache_handlers
|
||||
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()
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from common.extensions import db
|
||||
from common.models.user import Tenant
|
||||
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.traicie.knockout_questions.knockout_questions_v1_0 import KOQuestions, KOQuestion
|
||||
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||
@@ -177,8 +178,15 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||
"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,
|
||||
answer=f"We starten met een aantal KO Criteria vragen",
|
||||
answer=answer,
|
||||
form_request=ko_form,
|
||||
phase="ko_question_evaluation")
|
||||
|
||||
@@ -208,15 +216,27 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||
break
|
||||
|
||||
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,
|
||||
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,
|
||||
phase="no_valid_candidate")
|
||||
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
|
||||
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,
|
||||
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,
|
||||
phase="personal_contact_data")
|
||||
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
Reference in New Issue
Block a user