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

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

View File

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

View File

@@ -11,10 +11,85 @@
--spacing: 16px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
/* App container layout */
.app-container {
display: flex;
height: 100vh;
width: 100%;
}
/* Sidebar styling */
.sidebar {
width: 300px;
background-color: var(--sidebar-background);
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-logo {
text-align: center;
margin-bottom: 20px;
}
.sidebar-logo img {
max-width: 100%;
max-height: 100px;
}
.sidebar-make-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.sidebar-explanation {
margin-top: 20px;
overflow-y: auto;
background-color: var(--markdown-background-color);
color: var(--markdown-text-color);
padding: 10px;
border-radius: 5px;
}
/* Ensure all elements in the markdown content inherit the text color */
.sidebar-explanation * {
color: inherit;
}
/* Style links in the markdown content */
.sidebar-explanation a {
color: var(--primary-color);
text-decoration: underline;
}
/* Style lists in markdown content */
.sidebar-explanation ul,
.sidebar-explanation ol {
padding-left: 20px;
margin: 10px 0;
}
.sidebar-explanation li {
margin-bottom: 5px;
}
.sidebar-explanation ul li {
list-style-type: disc;
}
.sidebar-explanation ol li {
list-style-type: decimal;
}
.content-area {
flex: 1;
background: linear-gradient(135deg, var(--gradient-start-color), var(--gradient-end-color));
overflow-y: auto;
display: flex;
flex-direction: column;
}
body {
@@ -35,11 +110,14 @@ body {
.chat-container {
display: flex;
height: 100%;
flex: 1;
flex-direction: column;
min-height: 0;
}
.sidebar {
width: 280px;
background-color: var(--sidebar-color);
background-color: var(--sidebar-background);
border-right: 1px solid rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
@@ -98,6 +176,35 @@ body {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
/* Indicator voor taalwijziging */
.language-change-indicator {
background-color: rgba(var(--primary-color-rgb, 0, 123, 255), 0.2);
color: white;
padding: 5px 8px;
margin-bottom: 10px;
border-radius: 4px;
font-size: 0.9em;
text-align: center;
animation: fadeInOut 3s ease-in-out;
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
}
.language-change-indicator.success {
background: #d4edda;
color: #155724;
}
.language-change-indicator.error {
background: #f8d7da;
color: #721c24;
}
/* .chat-messages wordt nu gedefinieerd in chat-components.css */
/* .message wordt nu gedefinieerd in chat-components.css */
@@ -164,4 +271,4 @@ body {
/* .btn-primary wordt nu gedefinieerd in chat-components.css */
/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
/* Responsieve design regels worden nu gedefinieerd in chat-components.css */

View File

@@ -0,0 +1,50 @@
/* Styling voor de taalselector */
.sidebar-language-section {
padding: 10px 15px;
margin-bottom: 15px;
}
#language-selector-container {
display: flex;
flex-direction: column;
padding: 10px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 5px;
margin: 10px 0;
}
#language-selector-container label {
margin-bottom: 5px;
color: var(--sidebar-color);
font-size: 0.9rem;
font-weight: 500;
}
.language-selector {
padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.2);
color: var(--sidebar-color);
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.language-selector:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.language-selector:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.3);
}
.language-selector option {
background-color: #2c3e50;
color: white;
padding: 8px;
}

View File

@@ -5,6 +5,7 @@ import { DynamicForm } from '/static/assets/js/components/DynamicForm.js';
import { ChatMessage } from '/static/assets/js/components/ChatMessage.js';
import { 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);
});

View File

@@ -4,20 +4,20 @@
// Anders moet je ervoor zorgen dat MaterialIconManager.js eerder wordt geladen
// en iconManager beschikbaar is via window.iconManager
// Voeg stylesheet toe voor ChatInput-specifieke stijlen
const addStylesheet = () => {
if (!document.querySelector('link[href*="chat-input.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/static/assets/css/chat-input.css';
document.head.appendChild(link);
}
};
// Voeg stylesheet toe voor ChatInput-specifieke stijlen
const addStylesheet = () => {
if (!document.querySelector('link[href*="chat-input.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/static/assets/css/chat-input.css';
document.head.appendChild(link);
}
};
// Laad de stylesheet
addStylesheet();
// Laad de stylesheet
addStylesheet();
export const ChatInput = {
export const ChatInput = {
name: 'ChatInput',
components: {
'dynamic-form': window.DynamicForm
@@ -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"

View File

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

View File

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

View File

@@ -27,12 +27,21 @@ export const MessageHistory = {
data() {
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">

View File

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

View File

@@ -7,13 +7,17 @@
<!-- CSS -->
<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() {

View File

@@ -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
@@ -40,4 +43,4 @@
<!-- Import components and main app -->
<!-- Alle componenten worden geladen met absolute paden vanaf /static/ -->
<script type="module" src="{{ url_for('static', filename='assets/js/chat-app.js') }}"></script>
{% endblock %}
{% endblock %}

View File

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