Merge branch 'release/3.1.1-alfa'
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
@@ -50,8 +52,8 @@ class TranslationServices:
|
|||||||
if isinstance(config_data, str):
|
if isinstance(config_data, str):
|
||||||
config_data = json.loads(config_data)
|
config_data = json.loads(config_data)
|
||||||
|
|
||||||
# Maak een kopie van de originele data om te wijzigen
|
# Maak een deep copy van de originele data om te wijzigen en input-mutatie te vermijden
|
||||||
translated_config = config_data.copy()
|
translated_config = copy.deepcopy(config_data)
|
||||||
|
|
||||||
# Haal type en versie op voor de Business Event span
|
# Haal type en versie op voor de Business Event span
|
||||||
config_type = config_data.get('type', 'Unknown')
|
config_type = config_data.get('type', 'Unknown')
|
||||||
@@ -65,71 +67,124 @@ class TranslationServices:
|
|||||||
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
|
if not context and 'metadata' in config_data and 'description' in config_data['metadata']:
|
||||||
description_context = config_data['metadata']['description']
|
description_context = config_data['metadata']['description']
|
||||||
|
|
||||||
|
# Hulpfuncties
|
||||||
|
def is_nonempty_str(val: Any) -> bool:
|
||||||
|
return isinstance(val, str) and val.strip() != ''
|
||||||
|
|
||||||
|
def safe_translate(text: str, ctx: Optional[str]):
|
||||||
|
try:
|
||||||
|
res = cache_manager.translation_cache.get_translation(
|
||||||
|
text=text,
|
||||||
|
target_lang=target_language,
|
||||||
|
source_lang=source_language,
|
||||||
|
context=ctx
|
||||||
|
)
|
||||||
|
return res.translated_text if res else None
|
||||||
|
except Exception as e:
|
||||||
|
if current_event:
|
||||||
|
current_event.log_error('translation_error', {
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'config_type': config_type,
|
||||||
|
'config_version': config_version,
|
||||||
|
'field_config': field_config,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
return None
|
||||||
|
|
||||||
|
tag_pair_pattern = re.compile(r'<([a-zA-Z][\w-]*)>[\s\S]*?<\/\1>')
|
||||||
|
|
||||||
|
def extract_tag_counts(text: str) -> Dict[str, int]:
|
||||||
|
counts: Dict[str, int] = {}
|
||||||
|
for m in tag_pair_pattern.finditer(text or ''):
|
||||||
|
tag = m.group(1)
|
||||||
|
counts[tag] = counts.get(tag, 0) + 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def tags_valid(source: str, translated: str) -> bool:
|
||||||
|
return extract_tag_counts(source) == extract_tag_counts(translated)
|
||||||
|
|
||||||
|
# Counters
|
||||||
|
meta_consentRich_translated_count = 0
|
||||||
|
meta_aria_translated_count = 0
|
||||||
|
meta_inline_tags_invalid_after_translation_count = 0
|
||||||
|
|
||||||
# Loop door elk veld in de configuratie
|
# Loop door elk veld in de configuratie
|
||||||
for field_name, field_data in fields.items():
|
for field_name, field_data in fields.items():
|
||||||
# Vertaal name als het bestaat en niet leeg is
|
# Vertaal name als het bestaat en niet leeg is (alleen strings)
|
||||||
if 'name' in field_data and field_data['name']:
|
if 'name' in field_data and is_nonempty_str(field_data['name']):
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_name = cache_manager.translation_cache.get_translation(
|
t = safe_translate(field_data['name'], field_context)
|
||||||
text=field_data['name'],
|
if t:
|
||||||
target_lang=target_language,
|
translated_config[field_config][field_name]['name'] = t
|
||||||
source_lang=source_language,
|
|
||||||
context=field_context
|
|
||||||
)
|
|
||||||
if translated_name:
|
|
||||||
translated_config[field_config][field_name]['name'] = translated_name.translated_text
|
|
||||||
|
|
||||||
if 'title' in field_data and field_data['title']:
|
if 'title' in field_data and is_nonempty_str(field_data.get('title')):
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_title = cache_manager.translation_cache.get_translation(
|
t = safe_translate(field_data['title'], field_context)
|
||||||
text=field_data['title'],
|
if t:
|
||||||
target_lang=target_language,
|
translated_config[field_config][field_name]['title'] = t
|
||||||
source_lang=source_language,
|
|
||||||
context=field_context
|
|
||||||
)
|
|
||||||
if translated_title:
|
|
||||||
translated_config[field_config][field_name]['title'] = translated_title.translated_text
|
|
||||||
|
|
||||||
# Vertaal description als het bestaat en niet leeg is
|
# Vertaal description als het bestaat en niet leeg is
|
||||||
if 'description' in field_data and field_data['description']:
|
if 'description' in field_data and is_nonempty_str(field_data.get('description')):
|
||||||
# Gebruik context indien opgegeven, anders description_context
|
|
||||||
field_context = context if context else description_context
|
field_context = context if context else description_context
|
||||||
translated_desc = cache_manager.translation_cache.get_translation(
|
t = safe_translate(field_data['description'], field_context)
|
||||||
text=field_data['description'],
|
if t:
|
||||||
target_lang=target_language,
|
translated_config[field_config][field_name]['description'] = t
|
||||||
source_lang=source_language,
|
|
||||||
context=field_context
|
|
||||||
)
|
|
||||||
if translated_desc:
|
|
||||||
translated_config[field_config][field_name]['description'] = translated_desc.translated_text
|
|
||||||
|
|
||||||
# Vertaal context als het bestaat en niet leeg is
|
# Vertaal context als het bestaat en niet leeg is
|
||||||
if 'context' in field_data and field_data['context']:
|
if 'context' in field_data and is_nonempty_str(field_data.get('context')):
|
||||||
translated_ctx = cache_manager.translation_cache.get_translation(
|
t = safe_translate(field_data['context'], context)
|
||||||
text=field_data['context'],
|
if t:
|
||||||
target_lang=target_language,
|
translated_config[field_config][field_name]['context'] = t
|
||||||
source_lang=source_language,
|
|
||||||
context=context
|
|
||||||
)
|
|
||||||
if translated_ctx:
|
|
||||||
translated_config[field_config][field_name]['context'] = translated_ctx.translated_text
|
|
||||||
|
|
||||||
# vertaal allowed values als het veld bestaat en de waarden niet leeg zijn.
|
# vertaal allowed_values als het veld bestaat en waarden niet leeg zijn (alleen string-items)
|
||||||
if 'allowed_values' in field_data and field_data['allowed_values']:
|
if 'allowed_values' in field_data and isinstance(field_data['allowed_values'], list) and field_data['allowed_values']:
|
||||||
translated_allowed_values = []
|
translated_allowed_values = []
|
||||||
for allowed_value in field_data['allowed_values']:
|
for allowed_value in field_data['allowed_values']:
|
||||||
translated_allowed_value = cache_manager.translation_cache.get_translation(
|
if is_nonempty_str(allowed_value):
|
||||||
text=allowed_value,
|
t = safe_translate(allowed_value, context)
|
||||||
target_lang=target_language,
|
translated_allowed_values.append(t if t else allowed_value)
|
||||||
source_lang=source_language,
|
else:
|
||||||
context=context
|
translated_allowed_values.append(allowed_value)
|
||||||
)
|
|
||||||
translated_allowed_values.append(translated_allowed_value.translated_text)
|
|
||||||
if translated_allowed_values:
|
if translated_allowed_values:
|
||||||
translated_config[field_config][field_name]['allowed_values'] = translated_allowed_values
|
translated_config[field_config][field_name]['allowed_values'] = translated_allowed_values
|
||||||
|
|
||||||
|
# Vertaal meta.consentRich en meta.aria*
|
||||||
|
meta = field_data.get('meta')
|
||||||
|
if isinstance(meta, dict):
|
||||||
|
# consentRich
|
||||||
|
if is_nonempty_str(meta.get('consentRich')):
|
||||||
|
consent_ctx = (context if context else description_context) or ''
|
||||||
|
consent_ctx = f"Consent rich text with inline tags. Keep tag names intact and translate only inner text. {consent_ctx}".strip()
|
||||||
|
t = safe_translate(meta['consentRich'], consent_ctx)
|
||||||
|
if t and tags_valid(meta['consentRich'], t):
|
||||||
|
translated_config[field_config][field_name].setdefault('meta', {})['consentRich'] = t
|
||||||
|
meta_consentRich_translated_count += 1
|
||||||
|
else:
|
||||||
|
if t and not tags_valid(meta['consentRich'], t) and current_event:
|
||||||
|
src_counts = extract_tag_counts(meta['consentRich'])
|
||||||
|
dst_counts = extract_tag_counts(t)
|
||||||
|
current_event.log_error('inline_tags_validation_failed', {
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'config_type': config_type,
|
||||||
|
'config_version': config_version,
|
||||||
|
'field_config': field_config,
|
||||||
|
'field_name': field_name,
|
||||||
|
'target_language': target_language,
|
||||||
|
'source_tag_counts': src_counts,
|
||||||
|
'translated_tag_counts': dst_counts
|
||||||
|
})
|
||||||
|
meta_inline_tags_invalid_after_translation_count += 1
|
||||||
|
# fallback: keep original (already in deep copy)
|
||||||
|
# aria*
|
||||||
|
for k, v in list(meta.items()):
|
||||||
|
if isinstance(k, str) and k.startswith('aria') and is_nonempty_str(v):
|
||||||
|
aria_ctx = (context if context else description_context) or ''
|
||||||
|
aria_ctx = f"ARIA label for accessibility. Short, imperative, descriptive. Form '{config_type} {config_version}', field '{field_name}'. {aria_ctx}".strip()
|
||||||
|
t2 = safe_translate(v, aria_ctx)
|
||||||
|
if t2:
|
||||||
|
translated_config[field_config][field_name].setdefault('meta', {})[k] = t2
|
||||||
|
meta_aria_translated_count += 1
|
||||||
|
|
||||||
return translated_config
|
return translated_config
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ content: >
|
|||||||
|
|
||||||
'{context}'
|
'{context}'
|
||||||
|
|
||||||
Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
|
These are best practices you should follow:
|
||||||
Remove the triple quotes in your translation!
|
|
||||||
|
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
|
||||||
|
- We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "<terms_and_conditions>Terms & Conditions</terms_and_conditions>" translates in Dutch to <terms_and_conditions>Gebruiksvoorwaarden</terms_and_conditions>
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ content: >
|
|||||||
|
|
||||||
into '{target_language}'.
|
into '{target_language}'.
|
||||||
|
|
||||||
Do not translate text in between double square brackets, as these are names or terms that need to remain intact.
|
These are best practices you should follow:
|
||||||
Remove the triple quotes in your translation!
|
|
||||||
|
- Do not translate text in between double square brackets, as these are names or terms that need to remain intact. Remove the square brackets in the translation!
|
||||||
|
- We use inline tags (Custom HTML/XML-like tags). Ensure the tags themself are not translated and remain intact in the translation. The text inbetween the tags should be translated. e.g. "<terms_and_conditions>Terms & Conditions</terms_and_conditions>" translates in Dutch to <terms_and_conditions>Gebruiksvoorwaarden</terms_and_conditions>
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ fields:
|
|||||||
type: "boolean"
|
type: "boolean"
|
||||||
description: "Consent"
|
description: "Consent"
|
||||||
required: true
|
required: true
|
||||||
|
meta:
|
||||||
|
kind: "consent"
|
||||||
|
consentRich: "Ik Agree with the <terms>Terms and Conditions</terms> and the <privacy>Privacy Statement</privacy> of Ask Eve AI"
|
||||||
|
ariaPrivacy: "Open privacyverklaring in a modal dialog"
|
||||||
|
ariaTerms: "Open algemene voorwaarden in a modal dialog"
|
||||||
metadata:
|
metadata:
|
||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2025-07-29"
|
date_added: "2025-07-29"
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ All notable changes to EveAI will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.1.1-alfa]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- TRA-76 - Send Button color changes implemented
|
||||||
|
- TRA-72 - Translation of privacy statement and T&C
|
||||||
|
- TRA-73 - Strange characters in Tenant Make Name
|
||||||
|
- TRA-77 - Adapted Scroll behavior for Chat Client in Message History
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
## [3.1.0-alfa]
|
## [3.1.0-alfa]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -101,6 +101,19 @@ erDiagram
|
|||||||
int updated_by FK
|
int updated_by FK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EVE_AI_DATA_CAPSULE {
|
||||||
|
int id PK
|
||||||
|
int chat_session_id FK
|
||||||
|
string type
|
||||||
|
string type_version
|
||||||
|
jsonb configuration
|
||||||
|
jsonb data
|
||||||
|
datetime created_at
|
||||||
|
int created_by FK
|
||||||
|
datetime updated_at
|
||||||
|
int updated_by FK
|
||||||
|
}
|
||||||
|
|
||||||
DISPATCHER {
|
DISPATCHER {
|
||||||
int id PK
|
int id PK
|
||||||
string name
|
string name
|
||||||
@@ -188,24 +201,28 @@ erDiagram
|
|||||||
%% Main conversation flow
|
%% Main conversation flow
|
||||||
USER ||--o{ CHAT_SESSION : "has many"
|
USER ||--o{ CHAT_SESSION : "has many"
|
||||||
CHAT_SESSION ||--o{ INTERACTION : "has many"
|
CHAT_SESSION ||--o{ INTERACTION : "has many"
|
||||||
|
CHAT_SESSION ||--o{ EVE_AI_DATA_CAPSULE : "has many"
|
||||||
SPECIALIST ||--o{ INTERACTION : "processes"
|
SPECIALIST ||--o{ INTERACTION : "processes"
|
||||||
|
|
||||||
%% Specialist composition (EveAI components)
|
%% Specialist composition (EveAI components)
|
||||||
SPECIALIST ||--o{ EVE_AI_AGENT : "has many"
|
SPECIALIST ||--o{ EVE_AI_AGENT : "has many"
|
||||||
SPECIALIST ||--o{ EVE_AI_TASK : "has many"
|
SPECIALIST ||--o{ EVE_AI_TASK : "has many"
|
||||||
SPECIALIST ||--o{ EVE_AI_TOOL : "has many"
|
SPECIALIST ||--o{ EVE_AI_TOOL : "has many"
|
||||||
|
|
||||||
%% Specialist connections
|
%% Specialist connections
|
||||||
SPECIALIST ||--o{ SPECIALIST_RETRIEVER : "uses retrievers"
|
SPECIALIST ||--o{ SPECIALIST_RETRIEVER : "uses retrievers"
|
||||||
RETRIEVER ||--o{ SPECIALIST_RETRIEVER : "used by specialists"
|
RETRIEVER ||--o{ SPECIALIST_RETRIEVER : "used by specialists"
|
||||||
|
|
||||||
SPECIALIST ||--o{ SPECIALIST_DISPATCHER : "uses dispatchers"
|
SPECIALIST ||--o{ SPECIALIST_DISPATCHER : "uses dispatchers"
|
||||||
DISPATCHER ||--o{ SPECIALIST_DISPATCHER : "used by specialists"
|
DISPATCHER ||--o{ SPECIALIST_DISPATCHER : "used by specialists"
|
||||||
|
|
||||||
%% Interaction results
|
%% Interaction results
|
||||||
INTERACTION ||--o{ INTERACTION_EMBEDDING : "references embeddings"
|
INTERACTION ||--o{ INTERACTION_EMBEDDING : "references embeddings"
|
||||||
EMBEDDING ||--o{ INTERACTION_EMBEDDING : "used in interactions"
|
EMBEDDING ||--o{ INTERACTION_EMBEDDING : "used in interactions"
|
||||||
|
|
||||||
%% Magic links for specialist access
|
%% Magic links for specialist access
|
||||||
SPECIALIST ||--o{ SPECIALIST_MAGIC_LINK : "has magic links"
|
SPECIALIST ||--o{ SPECIALIST_MAGIC_LINK : "has magic links"
|
||||||
TENANT_MAKE ||--o{ SPECIALIST_MAGIC_LINK : "branded links"
|
TENANT_MAKE ||--o{ SPECIALIST_MAGIC_LINK : "branded links"
|
||||||
|
|
||||||
|
%% User relationships for audit trails
|
||||||
|
USER ||--o{ EVE_AI_DATA_CAPSULE : "created/updated by"
|
||||||
@@ -1,45 +1,3 @@
|
|||||||
|
|
||||||
/* Chat App Container Layout */
|
|
||||||
.chat-app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0; /* Belangrijk voor flexbox overflow */
|
|
||||||
padding: 20px; /* Algemene padding voor alle kanten */
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gemeenschappelijke container voor consistente breedte */
|
|
||||||
.chat-component-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px; /* Optimale breedte */
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1; /* Neemt beschikbare verticale ruimte in */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Message Area - neemt alle beschikbare ruimte */
|
|
||||||
.chat-messages-area {
|
|
||||||
flex: 1; /* Neemt alle beschikbare ruimte */
|
|
||||||
overflow: hidden; /* Voorkomt dat het groter wordt dan container */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0; /* Belangrijk voor nested flexbox */
|
|
||||||
margin-bottom: 20px; /* Ruimte tussen messages en input */
|
|
||||||
border-radius: 15px;
|
|
||||||
background: var(--history-background);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px; /* Optimale breedte */
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto; /* Horizontaal centreren */
|
|
||||||
align-self: center; /* Extra centrering in flexbox context */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat Input - altijd onderaan */
|
/* Chat Input - altijd onderaan */
|
||||||
.chat-input-area {
|
.chat-input-area {
|
||||||
flex: none; /* Neemt alleen benodigde ruimte */
|
flex: none; /* Neemt alleen benodigde ruimte */
|
||||||
@@ -56,14 +14,6 @@
|
|||||||
align-self: center; /* Extra centrering in flexbox context */
|
align-self: center; /* Extra centrering in flexbox context */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 10px; /* Ruimte voor scrollbar */
|
|
||||||
margin-right: -10px; /* Compenseer voor scrollbar */
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat Input styling */
|
/* Chat Input styling */
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
|
|||||||
@@ -99,15 +99,6 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat layout */
|
|
||||||
.chat-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
background-color: var(--sidebar-background);
|
background-color: var(--sidebar-background);
|
||||||
|
|||||||
@@ -549,20 +549,40 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.chat-app-container {
|
.chat-app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages-area {
|
.chat-messages-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px; /* Ruimte tussen messages en input */
|
||||||
|
border-radius: 15px;
|
||||||
|
background: var(--history-background);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px; /* Optimale breedte */
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto; /* Horizontaal centreren */
|
||||||
|
align-self: center; /* Extra centrering in flexbox context */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-area {
|
.chat-input-area {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||||
<img
|
<img
|
||||||
v-if="message.sender === 'ai'"
|
v-if="message.sender === 'ai'"
|
||||||
:src="staticUrl('assets/img/eveai_logo.svg')"
|
:src="staticUrl('/assets/img/eveai_logo.svg')"
|
||||||
alt="EveAI"
|
alt="EveAI"
|
||||||
class="ai-message-logo"
|
class="ai-message-logo"
|
||||||
/>
|
/>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||||
<img
|
<img
|
||||||
v-if="message.sender === 'ai'"
|
v-if="message.sender === 'ai'"
|
||||||
:src="staticUrl('assets/img/eveai_logo.svg')"
|
:src="staticUrl('/assets/img/eveai_logo.svg')"
|
||||||
alt="EveAI"
|
alt="EveAI"
|
||||||
class="ai-message-logo"
|
class="ai-message-logo"
|
||||||
/>
|
/>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||||
<img
|
<img
|
||||||
v-if="message.sender === 'ai'"
|
v-if="message.sender === 'ai'"
|
||||||
:src="staticUrl('assets/img/eveai_logo.svg')"
|
:src="staticUrl('/assets/img/eveai_logo.svg')"
|
||||||
alt="EveAI"
|
alt="EveAI"
|
||||||
class="ai-message-logo"
|
class="ai-message-logo"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<span class="consent-rich-text">
|
||||||
|
<template v-for="(node, idx) in nodes" :key="idx">
|
||||||
|
<component
|
||||||
|
v-if="node.type !== 'text'"
|
||||||
|
:is="linkTag"
|
||||||
|
:href="linkTag === 'a' ? '#' : undefined"
|
||||||
|
class="consent-link"
|
||||||
|
:aria-label="node.aria"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click.prevent="emitClick(node.type)"
|
||||||
|
@keydown.enter.prevent="emitClick(node.type)"
|
||||||
|
@keydown.space.prevent="emitClick(node.type)"
|
||||||
|
>{{ node.label }}</component>
|
||||||
|
<span v-else>{{ node.text }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ConsentRichText',
|
||||||
|
props: {
|
||||||
|
template: { type: String, required: true },
|
||||||
|
asButton: { type: Boolean, default: false },
|
||||||
|
ariaPrivacy: { type: String, default: 'Open privacy statement in a dialog' },
|
||||||
|
ariaTerms: { type: String, default: 'Open terms and conditions in a dialog' }
|
||||||
|
},
|
||||||
|
emits: ['open-privacy', 'open-terms'],
|
||||||
|
computed: {
|
||||||
|
linkTag() {
|
||||||
|
return this.asButton ? 'button' : 'a';
|
||||||
|
},
|
||||||
|
nodes() {
|
||||||
|
// Parse only allowed tags <privacy>...</privacy> and <terms>...</terms>
|
||||||
|
const source = (this.template || '');
|
||||||
|
|
||||||
|
// 2) parse only allowed tags <privacy>...</privacy> and <terms>...</terms>
|
||||||
|
const pattern = /<(privacy|terms)>([\s\S]*?)<\/\1>/gi;
|
||||||
|
const out = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(source)) !== null) {
|
||||||
|
const [full, tag, label] = match;
|
||||||
|
const start = match.index;
|
||||||
|
if (start > lastIndex) {
|
||||||
|
out.push({ type: 'text', text: source.slice(lastIndex, start) });
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
type: tag, // 'privacy' | 'terms'
|
||||||
|
label: (label || '').trim(),
|
||||||
|
aria: tag === 'privacy' ? this.ariaPrivacy : this.ariaTerms
|
||||||
|
});
|
||||||
|
lastIndex = start + full.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < source.length) {
|
||||||
|
out.push({ type: 'text', text: source.slice(lastIndex) });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emitClick(kind) {
|
||||||
|
if (kind === 'privacy') this.$emit('open-privacy');
|
||||||
|
if (kind === 'terms') this.$emit('open-terms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.consent-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
.consent-link:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
.consent-link:focus {
|
||||||
|
outline: 2px solid #007bff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
button.consent-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -104,14 +104,16 @@
|
|||||||
>
|
>
|
||||||
<!-- Regular checkbox label -->
|
<!-- Regular checkbox label -->
|
||||||
<span v-if="!isConsentField" class="checkbox-text">{{ field.name }}</span>
|
<span v-if="!isConsentField" class="checkbox-text">{{ field.name }}</span>
|
||||||
<!-- Consent field with privacy and terms links -->
|
<!-- Consent field with privacy and terms links (rich, multilingual) -->
|
||||||
<span v-else class="checkbox-text consent-text">
|
<ConsentRichText
|
||||||
{{ texts.consentPrefix }}
|
v-else
|
||||||
<a href="#" @click="openPrivacyModal" class="consent-link">{{ texts.privacyLink }}</a>
|
class="checkbox-text consent-text"
|
||||||
{{ texts.consentMiddle }}
|
:template="texts.consentRich"
|
||||||
<a href="#" @click="openTermsModal" class="consent-link">{{ texts.termsLink }}</a>
|
:aria-privacy="texts.ariaPrivacy || 'Open privacy statement in a dialog'"
|
||||||
{{ texts.consentSuffix }}
|
:aria-terms="texts.ariaTerms || 'Open terms and conditions in a dialog'"
|
||||||
</span>
|
@open-privacy="openPrivacyModal"
|
||||||
|
@open-terms="openTermsModal"
|
||||||
|
/>
|
||||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,9 +182,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
|
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
|
||||||
|
import ConsentRichText from './ConsentRichText.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormField',
|
name: 'FormField',
|
||||||
|
components: { ConsentRichText },
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -201,13 +205,11 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal', 'keydown-enter'],
|
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal', 'keydown-enter'],
|
||||||
setup() {
|
setup() {
|
||||||
// Consent text constants (English base)
|
// Consent text constants (English base) - rich template
|
||||||
const consentTexts = {
|
const consentTexts = {
|
||||||
consentPrefix: "I agree with the",
|
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
||||||
consentMiddle: "and",
|
ariaPrivacy: 'Open privacy statement in a dialog',
|
||||||
consentSuffix: "of AskEveAI",
|
ariaTerms: 'Open terms and conditions in a dialog'
|
||||||
privacyLink: "privacy statement",
|
|
||||||
termsLink: "terms and conditions"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -230,24 +232,36 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
texts() {
|
texts() {
|
||||||
// Robust consent texts that always return valid values
|
// Validate that consentRich exists and includes both required tags; otherwise fallback to English base
|
||||||
// Use translated texts if available and valid, otherwise use fallback
|
const hasValidRich = (t) => t && typeof t.consentRich === 'string'
|
||||||
if (this.translatedTexts && typeof this.translatedTexts === 'object') {
|
&& /<privacy>[\s\S]*?<\/privacy>/.test(t.consentRich)
|
||||||
const translated = this.translatedTexts;
|
&& /<terms>[\s\S]*?<\/terms>/.test(t.consentRich);
|
||||||
// Check if translated texts have all required properties
|
|
||||||
if (translated.consentPrefix && translated.consentMiddle && translated.consentSuffix &&
|
// 1) Prefer backend-provided rich string on the field's meta (already localized)
|
||||||
translated.privacyLink && translated.termsLink) {
|
const meta = this.field && this.field.meta ? this.field.meta : (this.field.i18n || null);
|
||||||
return translated;
|
if (meta && typeof meta.consentRich === 'string' && hasValidRich(meta)) {
|
||||||
}
|
return {
|
||||||
|
consentRich: meta.consentRich,
|
||||||
|
ariaPrivacy: meta.ariaPrivacy || this.fallbackTexts.ariaPrivacy,
|
||||||
|
ariaTerms: meta.ariaTerms || this.fallbackTexts.ariaTerms
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Otherwise, use client-side translated texts if available and valid
|
||||||
|
if (this.translatedTexts && typeof this.translatedTexts === 'object' && hasValidRich(this.translatedTexts)) {
|
||||||
|
return this.translatedTexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to English texts
|
// 3) Fallback to English texts (rich template)
|
||||||
return this.fallbackTexts || {
|
if (this.fallbackTexts && hasValidRich(this.fallbackTexts)) {
|
||||||
consentPrefix: "I agree with the",
|
return this.fallbackTexts;
|
||||||
consentMiddle: "and",
|
}
|
||||||
consentSuffix: "of AskEveAI",
|
|
||||||
privacyLink: "privacy statement",
|
// 4) Ultimate fallback (should not happen): provide a safe default
|
||||||
termsLink: "terms and conditions"
|
return {
|
||||||
|
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
||||||
|
ariaPrivacy: 'Open privacy statement in a dialog',
|
||||||
|
ariaTerms: 'Open terms and conditions in a dialog'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
@@ -317,12 +331,10 @@ export default {
|
|||||||
this.value = file;
|
this.value = file;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openPrivacyModal(event) {
|
openPrivacyModal() {
|
||||||
event.preventDefault();
|
|
||||||
this.$emit('open-privacy-modal');
|
this.$emit('open-privacy-modal');
|
||||||
},
|
},
|
||||||
openTermsModal(event) {
|
openTermsModal() {
|
||||||
event.preventDefault();
|
|
||||||
this.$emit('open-terms-modal');
|
this.$emit('open-terms-modal');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,6 @@
|
|||||||
|
|
||||||
<!-- Messages wrapper for bottom alignment -->
|
<!-- Messages wrapper for bottom alignment -->
|
||||||
<div class="messages-wrapper">
|
<div class="messages-wrapper">
|
||||||
<!-- Empty state (only show when no messages) -->
|
|
||||||
<div v-if="normalMessages.length === 0" class="empty-state">
|
|
||||||
<div class="empty-icon">💬</div>
|
|
||||||
<div class="empty-text">Nog geen berichten</div>
|
|
||||||
<div class="empty-subtext">Start een gesprek door een bericht te typen!</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Normal message list (excluding temporarily positioned AI messages) -->
|
<!-- Normal message list (excluding temporarily positioned AI messages) -->
|
||||||
<template v-if="normalMessages.length > 0">
|
<template v-if="normalMessages.length > 0">
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
@@ -87,7 +80,8 @@ export default {
|
|||||||
isAtBottom: true,
|
isAtBottom: true,
|
||||||
showScrollButton: false,
|
showScrollButton: false,
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
languageChangeHandler: null
|
languageChangeHandler: null,
|
||||||
|
_prevSnapshot: { length: 0, firstId: null, lastId: null },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -98,29 +92,41 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
messages: {
|
messages: {
|
||||||
handler(newMessages, oldMessages) {
|
async handler(newMessages, oldMessages) {
|
||||||
const hasNewMessages = newMessages.length > (oldMessages?.length || 0);
|
const prev = this._prevSnapshot || { length: 0, firstId: null, lastId: null };
|
||||||
|
const curr = this.makeSnapshot(newMessages);
|
||||||
// Always auto-scroll when new messages are added (regardless of current scroll position)
|
const container = this.$refs.messagesContainer;
|
||||||
if (this.autoScroll && hasNewMessages) {
|
|
||||||
// Double $nextTick for better DOM update synchronization
|
const lengthIncreased = curr.length > prev.length;
|
||||||
this.$nextTick(() => {
|
const lengthDecreased = curr.length < prev.length; // reset/trim
|
||||||
this.$nextTick(() => {
|
const appended = lengthIncreased && curr.lastId !== prev.lastId;
|
||||||
this.scrollToBottom(true);
|
const prepended = lengthIncreased && curr.firstId !== prev.firstId && curr.lastId === prev.lastId;
|
||||||
});
|
const mutatedLastSameLength = curr.length === prev.length && curr.lastId === prev.lastId;
|
||||||
});
|
|
||||||
|
if (prepended && container) {
|
||||||
|
// Load-more bovenaan: positie behouden
|
||||||
|
const before = container.scrollHeight;
|
||||||
|
await this.nextFrame();
|
||||||
|
await this.nextFrame();
|
||||||
|
this.preserveScrollOnPrepend(before);
|
||||||
|
} else if (appended) {
|
||||||
|
// Nieuw bericht onderaan: altijd naar beneden (eis)
|
||||||
|
await this.scrollToBottom(true, { smooth: true, retries: 2 });
|
||||||
|
} else if (mutatedLastSameLength) {
|
||||||
|
// Laatste bericht groeit (AI streaming/media). Alleen sticky als we al onderaan waren of autoScroll actief is
|
||||||
|
if (this.autoScroll || this.isAtBottom) {
|
||||||
|
await this.scrollToBottom(false, { smooth: true, retries: 2 });
|
||||||
}
|
}
|
||||||
|
} else if (lengthDecreased && this.autoScroll) {
|
||||||
|
// Lijst verkort (reset): terug naar onderen
|
||||||
|
await this.scrollToBottom(true, { smooth: false, retries: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
this._prevSnapshot = curr;
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: false
|
immediate: false,
|
||||||
},
|
},
|
||||||
isTyping(newVal) {
|
|
||||||
if (newVal && this.autoScroll) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
// Maak een benoemde handler voor betere cleanup
|
// Maak een benoemde handler voor betere cleanup
|
||||||
@@ -135,25 +141,36 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setupScrollListener();
|
this.setupScrollListener();
|
||||||
|
|
||||||
// Initial scroll to bottom
|
|
||||||
if (this.autoScroll) {
|
if (this.autoScroll) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => this.scrollToBottom(true, { smooth: false, retries: 2 }));
|
||||||
this.scrollToBottom();
|
}
|
||||||
});
|
|
||||||
|
this._onResize = this.debounce(() => {
|
||||||
|
if (this.autoScroll || this.isAtBottom) {
|
||||||
|
this.scrollToBottom(false, { smooth: true, retries: 1 });
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
window.addEventListener('resize', this._onResize);
|
||||||
|
|
||||||
|
const wrapper = this.$el.querySelector('.messages-wrapper');
|
||||||
|
if (window.ResizeObserver && wrapper) {
|
||||||
|
this._resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (this.autoScroll || this.isAtBottom) {
|
||||||
|
this.scrollToBottom(false, { smooth: true, retries: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._resizeObserver.observe(wrapper);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
// Cleanup scroll listener
|
|
||||||
const container = this.$refs.messagesContainer;
|
const container = this.$refs.messagesContainer;
|
||||||
if (container) {
|
if (container) container.removeEventListener('scroll', this.handleScroll);
|
||||||
container.removeEventListener('scroll', this.handleScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup language change listener
|
|
||||||
if (this.languageChangeHandler) {
|
if (this.languageChangeHandler) {
|
||||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||||
}
|
}
|
||||||
|
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||||
|
if (this._resizeObserver) this._resizeObserver.disconnect();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async handleLanguageChange(event) {
|
async handleLanguageChange(event) {
|
||||||
@@ -197,17 +214,52 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom(force = false) {
|
async nextFrame() {
|
||||||
|
await this.$nextTick();
|
||||||
|
await new Promise(r => requestAnimationFrame(r));
|
||||||
|
},
|
||||||
|
|
||||||
|
async scrollToBottom(force = false, { smooth = true, retries = 2 } = {}) {
|
||||||
const container = this.$refs.messagesContainer;
|
const container = this.$refs.messagesContainer;
|
||||||
if (container) {
|
if (!container) return;
|
||||||
// Use requestAnimationFrame for better timing
|
|
||||||
requestAnimationFrame(() => {
|
const doScroll = (instant = false) => {
|
||||||
container.scrollTop = container.scrollHeight;
|
const behavior = instant ? 'auto' : (smooth ? 'smooth' : 'auto');
|
||||||
this.isAtBottom = true;
|
container.scrollTo({ top: container.scrollHeight, behavior });
|
||||||
this.showScrollButton = false;
|
};
|
||||||
this.unreadCount = 0;
|
|
||||||
});
|
// Wacht enkele frames om late layout/afbeeldingen te vangen
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
await this.nextFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Probeer smooth naar onder
|
||||||
|
doScroll(false);
|
||||||
|
|
||||||
|
// Forceer desnoods na nog een frame een harde correctie
|
||||||
|
if (force) {
|
||||||
|
await this.nextFrame();
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isAtBottom = true;
|
||||||
|
this.showScrollButton = false;
|
||||||
|
this.unreadCount = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSnapshot(list) {
|
||||||
|
const length = list.length;
|
||||||
|
const firstId = length ? list[0].id : null;
|
||||||
|
const lastId = length ? list[length - 1].id : null;
|
||||||
|
return { length, firstId, lastId };
|
||||||
|
},
|
||||||
|
|
||||||
|
preserveScrollOnPrepend(beforeHeight) {
|
||||||
|
const container = this.$refs.messagesContainer;
|
||||||
|
if (!container) return;
|
||||||
|
const afterHeight = container.scrollHeight;
|
||||||
|
const delta = afterHeight - beforeHeight;
|
||||||
|
container.scrollTop = container.scrollTop + delta;
|
||||||
},
|
},
|
||||||
|
|
||||||
setupScrollListener() {
|
setupScrollListener() {
|
||||||
@@ -217,19 +269,21 @@ export default {
|
|||||||
container.addEventListener('scroll', this.handleScroll);
|
container.addEventListener('scroll', this.handleScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
debounce(fn, wait = 150) {
|
||||||
|
let t;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(() => fn.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
const container = this.$refs.messagesContainer;
|
const container = this.$refs.messagesContainer;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
const threshold = 80; // was 50
|
||||||
const threshold = 50; // Reduced threshold for better detection
|
|
||||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||||
|
|
||||||
this.isAtBottom = isNearBottom;
|
this.isAtBottom = isNearBottom;
|
||||||
|
if (container.scrollTop === 0) this.$emit('load-more');
|
||||||
// Load more messages when scrolled to top
|
|
||||||
if (container.scrollTop === 0) {
|
|
||||||
this.$emit('load-more');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleImageLoaded() {
|
handleImageLoaded() {
|
||||||
@@ -274,35 +328,39 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.message-history-container {
|
.message-history-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
min-height: 0;
|
height: 100%;
|
||||||
padding: 20px; /* Interne padding voor MessageHistory */
|
min-height: 0; /* Laat kinderen scrollen */
|
||||||
box-sizing: border-box;
|
padding: 20px;
|
||||||
width: 100%;
|
box-sizing: border-box;
|
||||||
max-width: 1000px; /* Optimale breedte */
|
width: 100%;
|
||||||
margin-left: auto;
|
max-width: 1000px;
|
||||||
margin-right: auto; /* Horizontaal centreren */
|
margin-left: auto;
|
||||||
overflow: hidden;
|
margin-right: auto;
|
||||||
|
/* overflow: hidden; // mag weg of blijven; met stap 1 clipt dit niet meer */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
|
||||||
scroll-behavior: smooth;
|
scrollbar-gutter: stable both-edges; /* houdt ruimte vrij voor scrollbar */
|
||||||
|
padding-right: 0; /* haal de hack weg */
|
||||||
/* Bottom-aligned messages implementation */
|
margin-right: 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
/* Belangrijk: haal min-height: 100% weg en vervang */
|
||||||
justify-content: flex-end;
|
/* min-height: 100%; */
|
||||||
min-height: 100%;
|
min-height: 0; /* toestaan dat het kind krimpt voor overflow */
|
||||||
|
-webkit-overflow-scrolling: touch; /* betere iOS scroll */
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-wrapper {
|
.messages-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 100%;
|
||||||
gap: 10px; /* Space between messages */
|
gap: 10px; /* Space between messages */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="progress-title">
|
<div class="progress-title">
|
||||||
<!-- Evie working animatie tijdens processing -->
|
<!-- Evie working animatie tijdens processing -->
|
||||||
<img v-if="isProcessing"
|
<img v-if="isProcessing"
|
||||||
:src="staticUrl('assets/img/evie_working.webp')"
|
:src="staticUrl('/assets/img/evie_working.webp')"
|
||||||
alt="Bezig met verwerken..."
|
alt="Bezig met verwerken..."
|
||||||
class="progress-icon working-animation">
|
class="progress-icon working-animation">
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
<!-- Alleen Evie animatie voor "No Information" tijdens processing -->
|
<!-- Alleen Evie animatie voor "No Information" tijdens processing -->
|
||||||
<div v-else-if="shouldShowProgressIconOnly" class="progress-icon-only">
|
<div v-else-if="shouldShowProgressIconOnly" class="progress-icon-only">
|
||||||
<img :src="staticUrl('assets/img/evie_working.webp')"
|
<img :src="staticUrl('/assets/img/evie_working.webp')"
|
||||||
alt="Bezig met verwerken..."
|
alt="Bezig met verwerken..."
|
||||||
class="working-animation-only">
|
class="working-animation-only">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<script>
|
<script>
|
||||||
// Definieer chatConfig voordat componenten worden geladen
|
// Definieer chatConfig voordat componenten worden geladen
|
||||||
window.chatConfig = {
|
window.chatConfig = {
|
||||||
explanation: `{{ customisation.sidebar_markdown|default('') }}`,
|
explanation: {{ customisation.sidebar_markdown|default('')|tojson }},
|
||||||
progress_tracker_insights: `{{ customisation.progress_tracker_insights|default('No Information') }}`,
|
progress_tracker_insights: {{ customisation.progress_tracker_insights|default('No Information')|tojson }},
|
||||||
form_title_display: `{{ customisation.form_title_display|default('Full Title') }}`,
|
form_title_display: {{ customisation.form_title_display|default('Full Title')|tojson }},
|
||||||
conversationId: '{{ conversation_id|default("default") }}',
|
conversationId: {{ conversation_id|default('default')|tojson }},
|
||||||
messages: {{ messages|tojson|safe }},
|
messages: {{ messages|tojson|safe }},
|
||||||
settings: {
|
settings: {
|
||||||
maxMessageLength: {{ settings.max_message_length|default(2000) }},
|
maxMessageLength: {{ settings.max_message_length|default(2000) }},
|
||||||
@@ -22,15 +22,15 @@
|
|||||||
allowReactions: {{ settings.allow_reactions|default('true')|lower }}
|
allowReactions: {{ settings.allow_reactions|default('true')|lower }}
|
||||||
},
|
},
|
||||||
apiPrefix: '/chat-client/chat',
|
apiPrefix: '/chat-client/chat',
|
||||||
language: '{{ session.magic_link.specialist_args.language|default("en") }}',
|
language: {{ session.magic_link.specialist_args.language|default('en')|tojson }},
|
||||||
supportedLanguageDetails: {{ config.SUPPORTED_LANGUAGE_DETAILS|tojson|safe }},
|
supportedLanguageDetails: {{ config.SUPPORTED_LANGUAGE_DETAILS|tojson|safe }},
|
||||||
allowedLanguages: {{ tenant_make.allowed_languages|tojson|safe }},
|
allowedLanguages: {{ tenant_make.allowed_languages|tojson|safe }},
|
||||||
tenantMake: {
|
tenantMake: {{ {
|
||||||
name: "{{ tenant_make.name|default('EveAI') }}",
|
'name': tenant_make.name or 'EveAI',
|
||||||
logo_url: "{{ tenant_make.logo_url|default('') }}"
|
'logo_url': tenant_make.logo_url or ''
|
||||||
},
|
}|tojson|safe }},
|
||||||
// Environment-aware static base provided by Flask's overridden url_for
|
// Environment-aware static base provided by Flask's overridden url_for
|
||||||
staticBase: '{{ static_url }}'
|
staticBase: {{ static_url|tojson }}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug info om te controleren of chatConfig correct is ingesteld
|
// Debug info om te controleren of chatConfig correct is ingesteld
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
try { globalThis.staticUrl = window.staticUrl; } catch (e) {}
|
try { globalThis.staticUrl = window.staticUrl; } catch (e) {}
|
||||||
} else {
|
} else {
|
||||||
// Prefer runtime chatConfig.staticBase; else fallback to server-provided base or default
|
// Prefer runtime chatConfig.staticBase; else fallback to server-provided base or default
|
||||||
var serverStaticBase = '{{ static_url|default("") }}' || '';
|
var serverStaticBase = {{ static_url|default('')|tojson }} || '';
|
||||||
if (!serverStaticBase) { serverStaticBase = '{{ url_for("static", filename="") }}'; }
|
if (!serverStaticBase) { serverStaticBase = {{ url_for('static', filename='')|tojson }}; }
|
||||||
var base = (window.chatConfig && window.chatConfig.staticBase) ? window.chatConfig.staticBase : (serverStaticBase || '/static/');
|
var base = (window.chatConfig && window.chatConfig.staticBase) ? window.chatConfig.staticBase : (serverStaticBase || '/static/');
|
||||||
var normalizedBase = String(base).replace(/\/+$/, '/');
|
var normalizedBase = String(base).replace(/\/+$/, '/');
|
||||||
window.staticUrl = function(path) {
|
window.staticUrl = function(path) {
|
||||||
@@ -34,19 +34,19 @@
|
|||||||
window.chatConfig.supportedLanguages = [
|
window.chatConfig.supportedLanguages = [
|
||||||
{% for lang_code in config.SUPPORTED_LANGUAGES %}
|
{% for lang_code in config.SUPPORTED_LANGUAGES %}
|
||||||
{
|
{
|
||||||
code: "{{ lang_code }}",
|
code: {{ lang_code|tojson }},
|
||||||
name: "{{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['iso 639-1'] }}",
|
name: {{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['iso 639-1']|tojson }},
|
||||||
flag: "{{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['flag'] }}"
|
flag: {{ config.SUPPORTED_LANGUAGE_DETAILS[config.SUPPORTED_LANGUAGES_FULL[loop.index0]]['flag']|tojson }}
|
||||||
}{% if not loop.last %},{% endif %}
|
}{% if not loop.last %},{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Voeg tenantMake toe aan chatConfig als die nog niet bestaat
|
// Voeg tenantMake toe aan chatConfig als die nog niet bestaat
|
||||||
if (!window.chatConfig.tenantMake) {
|
if (!window.chatConfig.tenantMake) {
|
||||||
window.chatConfig.tenantMake = {
|
window.chatConfig.tenantMake = {{ {
|
||||||
name: "{{ tenant_make.name|default('EveAI') }}",
|
'name': tenant_make.name or 'EveAI',
|
||||||
logo_url: "{{ tenant_make.logo_url|default('') }}"
|
'logo_url': tenant_make.logo_url or ''
|
||||||
};
|
}|tojson|safe }};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Taalinstellingen toegevoegd aan chatConfig');
|
console.log('Taalinstellingen toegevoegd aan chatConfig');
|
||||||
|
|||||||
@@ -89,18 +89,19 @@
|
|||||||
|
|
||||||
/* General Styles */
|
/* General Styles */
|
||||||
.chat-container {
|
.chat-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 75vh;
|
height: 75vh;
|
||||||
/*max-height: 100vh;*/
|
min-height: 0;
|
||||||
max-width: 600px;
|
/*max-height: 100vh;*/
|
||||||
margin: auto;
|
max-width: 600px;
|
||||||
border: 1px solid #ccc;
|
margin: auto;
|
||||||
border-radius: 8px;
|
border: 1px solid #ccc;
|
||||||
overflow: hidden;
|
border-radius: 8px;
|
||||||
background-color: var(--chat-bg);
|
overflow: hidden;
|
||||||
font-family: var(--font-family); /* Apply the default font family */
|
background-color: var(--chat-bg);
|
||||||
color: var(--font-color); /* Apply the default font color */
|
font-family: var(--font-family); /* Apply the default font family */
|
||||||
|
color: var(--font-color); /* Apply the default font color */
|
||||||
}
|
}
|
||||||
|
|
||||||
.disclaimer {
|
.disclaimer {
|
||||||
|
|||||||
Reference in New Issue
Block a user