- iconManager MaterialIconManager.js zijn nu 'unified' in 1 component, en samen met translation utilities omgezet naar een meer moderne Vue composable

- De sidebar is nu eveneens omgezet naar een Vue component.
This commit is contained in:
Josako
2025-07-20 18:07:17 +02:00
parent ccb844c15c
commit e75c49d2fa
24 changed files with 2358 additions and 413 deletions

View File

@@ -62,15 +62,26 @@
<script>
// Importeer de benodigde componenten
import DynamicForm from './DynamicForm.vue';
import { IconManagerMixin } from '../js/iconManager.js';
import { useIconManager } from '../js/composables/useIconManager.js';
import { useTranslationClient } from '../js/composables/useTranslation.js';
export default {
name: 'ChatInput',
components: {
'dynamic-form': DynamicForm
},
// Gebruik de IconManagerMixin om automatisch iconen te laden
mixins: [IconManagerMixin],
setup(props) {
const { watchIcon } = useIconManager();
const { translateSafe, isTranslating: isTranslatingText } = useTranslationClient();
// Watch formData.icon for automatic icon loading
watchIcon(() => props.formData?.icon);
return {
translateSafe,
isTranslatingText
};
},
props: {
currentMessage: {
type: String,
@@ -139,14 +150,6 @@ export default {
}
},
watch: {
'formData.icon': {
handler(newIcon) {
if (newIcon && window.iconManager) {
window.iconManager.ensureIconsLoaded({}, [newIcon]);
}
},
immediate: true
},
formData: {
handler(newFormData, oldFormData) {
console.log('ChatInput formData changed:', newFormData);
@@ -186,7 +189,7 @@ export default {
}
},
created() {
// Als er een formData.icon is, wordt deze automatisch geladen via IconManagerMixin
// Als er een formData.icon is, wordt deze automatisch geladen via useIconManager composable
// Geen expliciete window.iconManager calls meer nodig
// Maak een benoemde handler voor betere cleanup
@@ -234,30 +237,21 @@ export default {
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
// Gebruik moderne translateSafe composable
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
);
const translatedText = await this.translateSafe(originalText, language, {
context: 'chat_input_placeholder',
apiPrefix,
fallbackText: originalText
});
if (response.success) {
// Update de placeholder
this.translatedPlaceholder = response.translated_text;
} else {
console.error('Vertaling placeholder mislukt:', response.error);
}
// Update de placeholder
this.translatedPlaceholder = translatedText;
console.log('Placeholder succesvol vertaald naar:', language);
} catch (error) {
console.error('Fout bij vertalen placeholder:', error);
// Fallback naar originele tekst
this.translatedPlaceholder = originalText;
} finally {
// Reset de vertaling vlag
this.isTranslating = false;

View File

@@ -115,6 +115,7 @@
// Import benodigde componenten
import DynamicForm from './DynamicForm.vue';
import ProgressTracker from './ProgressTracker.vue';
import { useIconManager } from '../js/composables/useIconManager.js';
export default {
name: 'ChatMessage',
@@ -122,6 +123,14 @@ export default {
'dynamic-form': DynamicForm,
'progress-tracker': ProgressTracker
},
setup(props) {
const { watchIcon } = useIconManager();
// Watch message.formData.icon for automatic icon loading
watchIcon(() => props.message.formData?.icon);
return {};
},
props: {
message: {
type: Object,
@@ -146,11 +155,8 @@ export default {
};
},
created() {
// Zorg ervoor dat het icoon geladen wordt als iconManager beschikbaar is
if (window.iconManager && this.message.formData && this.message.formData.icon) {
window.iconManager.loadIcon(this.message.formData.icon);
}
// Icon loading is now handled automatically by useIconManager composable
// 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;
@@ -174,16 +180,6 @@ export default {
return this.message.formValues && Object.keys(this.message.formValues).length > 0;
}
},
watch: {
'message.formData.icon': {
handler(newIcon) {
if (newIcon && window.iconManager) {
window.iconManager.loadIcon(newIcon);
}
},
immediate: true
}
},
methods: {
async handleLanguageChange(event) {
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht

View File

@@ -73,12 +73,21 @@
<script>
import FormField from './FormField.vue';
import { useIconManager } from '../js/composables/useIconManager.js';
export default {
name: 'DynamicForm',
components: {
'form-field': FormField
},
setup(props) {
const { watchIcon } = useIconManager();
// Watch formData.icon for automatic icon loading
watchIcon(() => props.formData?.icon);
return {};
},
props: {
formData: {
type: Object,
@@ -189,20 +198,9 @@ export default {
},
deep: true
},
'formData.icon': {
handler(newIcon) {
if (newIcon && window.iconManager) {
window.iconManager.loadIcon(newIcon);
}
},
immediate: true
}
},
created() {
// Zorg ervoor dat het icoon geladen wordt als iconManager beschikbaar is
if (window.iconManager && this.formData && this.formData.icon) {
window.iconManager.loadIcon(this.formData.icon);
}
// Icon loading is now handled automatically by useIconManager composable
},
methods: {
updateFieldValue(fieldId, value) {

View File

@@ -39,6 +39,7 @@
<script>
import ChatMessage from './ChatMessage.vue';
import TypingIndicator from './TypingIndicator.vue';
import { useTranslationClient } from '../js/composables/useTranslation.js';
export default {
name: 'MessageHistory',
@@ -46,6 +47,13 @@ export default {
'chat-message': ChatMessage,
'typing-indicator': TypingIndicator
},
setup() {
const { translateSafe } = useTranslationClient();
return {
translateSafe
};
},
props: {
messages: {
type: Array,
@@ -141,30 +149,24 @@ export default {
console.log('Vertaling van eerste AI bericht naar:', event.detail.language);
try {
// Controleer of TranslationClient beschikbaar is
if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
console.error('TranslationClient.translate is niet beschikbaar');
return;
}
// Gebruik TranslationClient
const response = await window.TranslationClient.translate(
// Gebruik moderne translateSafe composable
const translatedText = await this.translateSafe(
firstMessage.originalContent,
event.detail.language,
null, // source_lang (auto-detect)
'chat_message', // context
this.apiPrefix // API prefix voor tenant routing
{
context: 'chat_message',
apiPrefix: this.apiPrefix,
fallbackText: firstMessage.originalContent
}
);
if (response.success) {
// Update de inhoud van het bericht
firstMessage.content = response.translated_text;
console.log('Eerste bericht succesvol vertaald');
} else {
console.error('Vertaling mislukt:', response.error);
}
// Update de inhoud van het bericht
firstMessage.content = translatedText;
console.log('Eerste bericht succesvol vertaald');
} catch (error) {
console.error('Fout bij vertalen eerste bericht:', error);
// Fallback naar originele inhoud
firstMessage.content = firstMessage.originalContent;
}
}
}

View File

@@ -0,0 +1,111 @@
<!-- SideBar.vue -->
<template>
<div class="sidebar">
<SideBarLogo
:logo-url="tenantMake.logo_url"
:make-name="tenantMake.name"
/>
<SideBarMakeName
:make-name="tenantMake.name"
:subtitle="tenantMake.subtitle"
/>
<LanguageSelector
:initial-language="initialLanguage"
:current-language="currentLanguage"
:supported-language-details="supportedLanguageDetails"
:allowed-languages="allowedLanguages"
@language-changed="handleLanguageChange"
/>
<SideBarExplanation
:original-text="explanationText"
:current-language="currentLanguage"
:api-prefix="apiPrefix"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
import SideBarLogo from './SideBarLogo.vue';
import SideBarMakeName from './SideBarMakeName.vue';
import LanguageSelector from './LanguageSelector.vue';
import SideBarExplanation from './SideBarExplanation.vue';
const props = defineProps({
tenantMake: {
type: Object,
default: () => ({
name: '',
logo_url: '',
subtitle: ''
})
},
explanationText: {
type: String,
default: ''
},
initialLanguage: {
type: String,
default: 'nl'
},
supportedLanguageDetails: {
type: Object,
default: () => ({})
},
allowedLanguages: {
type: Array,
default: () => ['nl', 'en', 'fr', 'de']
},
apiPrefix: {
type: String,
default: ''
}
});
const emit = defineEmits(['language-changed']);
const currentLanguage = ref(props.initialLanguage);
const handleLanguageChange = (newLanguage) => {
currentLanguage.value = newLanguage;
// Emit to parent
emit('language-changed', newLanguage);
// Global event for backward compatibility
const globalEvent = new CustomEvent('language-changed', {
detail: { language: newLanguage }
});
document.dispatchEvent(globalEvent);
// Update chatConfig
if (window.chatConfig) {
window.chatConfig.language = newLanguage;
}
// Save preference
localStorage.setItem('preferredLanguage', newLanguage);
};
</script>
<style scoped>
.sidebar {
display: flex;
flex-direction: column;
height: 100%;
background: var(--sidebar-background);
color: var(--sidebar-color);
width: 300px;
min-width: 250px;
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
min-width: unset;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<!-- SideBarExplanation.vue -->
<template>
<div class="sidebar-explanation">
<div
v-if="isLoading"
class="explanation-loading"
>
🔄 Translating...
</div>
<div
v-else
class="explanation-content"
v-html="renderedExplanation"
></div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useTranslationClient } from '../js/composables/useTranslation.js';
const props = defineProps({
originalText: {
type: String,
default: ''
},
currentLanguage: {
type: String,
default: 'nl'
},
apiPrefix: {
type: String,
default: ''
}
});
const { translateSafe } = useTranslationClient();
const translatedText = ref(props.originalText);
const isLoading = ref(false);
// Render markdown content
const renderedExplanation = computed(() => {
if (!translatedText.value) return '';
// Use marked if available, otherwise return plain text
if (typeof window.marked === 'function') {
return window.marked(translatedText.value);
} else if (window.marked && typeof window.marked.parse === 'function') {
return window.marked.parse(translatedText.value.replace(/\[\[(.*?)\]\]/g, '<strong>$1</strong>'));
} else {
return translatedText.value;
}
});
// Watch for language changes
watch(() => props.currentLanguage, async (newLanguage) => {
await updateTranslation(newLanguage);
});
// Watch for text changes
watch(() => props.originalText, async () => {
await updateTranslation(props.currentLanguage);
});
const updateTranslation = async (targetLanguage) => {
if (!props.originalText || targetLanguage === 'nl') {
translatedText.value = props.originalText;
return;
}
isLoading.value = true;
try {
const result = await translateSafe(props.originalText, targetLanguage, {
context: 'sidebar_explanation',
apiPrefix: props.apiPrefix,
fallbackText: props.originalText
});
translatedText.value = result;
} catch (error) {
console.warn('Sidebar explanation translation failed:', error);
translatedText.value = props.originalText;
} finally {
isLoading.value = false;
}
};
onMounted(() => {
updateTranslation(props.currentLanguage);
});
</script>
<style scoped>
.sidebar-explanation {
padding: 15px;
flex: 1;
overflow-y: auto;
}
.explanation-loading {
color: var(--sidebar-color);
font-style: italic;
text-align: center;
padding: 20px;
}
.explanation-content {
color: var(--sidebar-color);
font-size: 0.9rem;
line-height: 1.5;
}
.explanation-content :deep(h1),
.explanation-content :deep(h2),
.explanation-content :deep(h3) {
color: var(--sidebar-color);
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.explanation-content :deep(p) {
margin-bottom: 1rem;
}
.explanation-content :deep(strong) {
color: var(--primary-color);
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,73 @@
<!-- SideBarLogo.vue -->
<template>
<div class="sidebar-logo">
<img
v-if="logoUrl"
:src="logoUrl"
:alt="altText"
@error="handleImageError"
class="logo-image"
>
<div v-else class="logo-placeholder">
{{ makeNameInitials }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
logoUrl: {
type: String,
default: ''
},
makeName: {
type: String,
default: 'Logo'
}
});
const altText = computed(() => props.makeName || 'Logo');
const makeNameInitials = computed(() => {
return props.makeName
.split(' ')
.map(word => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
});
const handleImageError = () => {
console.warn('Logo image failed to load:', props.logoUrl);
};
</script>
<style scoped>
.sidebar-logo {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo-image {
max-width: 100%;
max-height: 60px;
object-fit: contain;
}
.logo-placeholder {
width: 50px;
height: 50px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 1.2rem;
}
</style>

View File

@@ -0,0 +1,42 @@
<!-- SideBarMakeName.vue -->
<template>
<div class="sidebar-make-name">
<h2 class="make-name-text">{{ makeName }}</h2>
<div v-if="subtitle" class="make-subtitle">{{ subtitle }}</div>
</div>
</template>
<script setup>
const props = defineProps({
makeName: {
type: String,
default: ''
},
subtitle: {
type: String,
default: ''
}
});
</script>
<style scoped>
.sidebar-make-name {
padding: 15px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.make-name-text {
color: var(--sidebar-color);
font-size: 1.1rem;
font-weight: 600;
margin: 0;
line-height: 1.3;
}
.make-subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 0.85rem;
margin-top: 5px;
}
</style>