- 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:
@@ -1,65 +0,0 @@
|
||||
// static/js/components/MaterialIconManager.js
|
||||
|
||||
/**
|
||||
* Een hulpklasse om Material Symbols Outlined iconen te beheren
|
||||
* en dynamisch toe te voegen aan de pagina indien nodig.
|
||||
*/
|
||||
export const MaterialIconManager = {
|
||||
name: 'MaterialIconManager',
|
||||
data() {
|
||||
return {
|
||||
loadedIconSets: [],
|
||||
defaultOptions: {
|
||||
opsz: 24, // Optimale grootte: 24px
|
||||
wght: 400, // Gewicht: normaal
|
||||
FILL: 0, // Vulling: niet gevuld
|
||||
GRAD: 0 // Kleurverloop: geen
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Zorgt ervoor dat de Material Symbols Outlined stijlbladen zijn geladen
|
||||
* @param {Object} options - Opties voor het icoon (opsz, wght, FILL, GRAD)
|
||||
* @param {Array} iconNames - Optionele lijst met specifieke iconen om te laden
|
||||
*/
|
||||
ensureIconsLoaded(options = {}, iconNames = []) {
|
||||
const opts = { ...this.defaultOptions, ...options };
|
||||
const styleUrl = this.buildStyleUrl(opts, iconNames);
|
||||
|
||||
// Controleer of deze specifieke set al is geladen
|
||||
if (!this.loadedIconSets.includes(styleUrl)) {
|
||||
this.loadStylesheet(styleUrl);
|
||||
this.loadedIconSets.push(styleUrl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Bouwt de URL voor het stijlblad
|
||||
*/
|
||||
buildStyleUrl(options, iconNames = []) {
|
||||
let url = `https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@${options.opsz},${options.wght},${options.FILL},${options.GRAD}`;
|
||||
|
||||
// Voeg specifieke iconNames toe als deze zijn opgegeven
|
||||
if (iconNames.length > 0) {
|
||||
url += `&icon_names=${iconNames.join(',')}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Laadt een stijlblad dynamisch
|
||||
*/
|
||||
loadStylesheet(url) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
console.log(`Material Symbols Outlined geladen: ${url}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Singleton instantie om te gebruiken in de hele applicatie
|
||||
export const iconManager = new Vue(MaterialIconManager);
|
||||
26
eveai_chat_client/static/assets/js/composables/index.js
Normal file
26
eveai_chat_client/static/assets/js/composables/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// eveai_chat_client/static/assets/js/composables/index.js
|
||||
|
||||
/**
|
||||
* Vue 3 Composables Barrel Export
|
||||
* Provides easy access to all composables
|
||||
*/
|
||||
|
||||
// Icon Management Composables
|
||||
export {
|
||||
useIconManager,
|
||||
useIcon,
|
||||
useFormIcon
|
||||
} from './useIconManager.js';
|
||||
|
||||
// Translation Management Composables
|
||||
export {
|
||||
useTranslation,
|
||||
useTranslationClient,
|
||||
useReactiveTranslation
|
||||
} from './useTranslation.js';
|
||||
|
||||
// Future composables can be added here:
|
||||
// export { useFormValidation } from './useFormValidation.js';
|
||||
// export { useChat } from './useChat.js';
|
||||
|
||||
console.log('Vue 3 composables loaded successfully');
|
||||
192
eveai_chat_client/static/assets/js/composables/useIconManager.js
Normal file
192
eveai_chat_client/static/assets/js/composables/useIconManager.js
Normal file
@@ -0,0 +1,192 @@
|
||||
// eveai_chat_client/static/assets/js/composables/useIconManager.js
|
||||
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Vue 3 Composable for Material Symbols Outlined icon management
|
||||
* Self-contained modern icon management without legacy dependencies
|
||||
*/
|
||||
export function useIconManager() {
|
||||
const isIconManagerReady = ref(true); // Always ready since we're self-contained
|
||||
const loadedIcons = ref([]);
|
||||
|
||||
/**
|
||||
* Load a single Material Symbols Outlined icon
|
||||
* @param {string} iconName - Name of the icon to load
|
||||
* @param {Object} options - Icon options (opsz, wght, FILL, GRAD)
|
||||
*/
|
||||
const loadIcon = (iconName, options = {}) => {
|
||||
if (!iconName) {
|
||||
console.warn('No icon name provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if icon is already loaded
|
||||
if (loadedIcons.value.includes(iconName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default options for Material Symbols Outlined
|
||||
const defaultOptions = {
|
||||
opsz: 24,
|
||||
wght: 400,
|
||||
FILL: 0,
|
||||
GRAD: 0
|
||||
};
|
||||
|
||||
const iconOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// Create CSS for the icon with specific options
|
||||
const cssRule = `
|
||||
.material-symbols-outlined.icon-${iconName} {
|
||||
font-variation-settings:
|
||||
'FILL' ${iconOptions.FILL},
|
||||
'wght' ${iconOptions.wght},
|
||||
'GRAD' ${iconOptions.GRAD},
|
||||
'opsz' ${iconOptions.opsz};
|
||||
}
|
||||
`;
|
||||
|
||||
// Add CSS rule to document
|
||||
const style = document.createElement('style');
|
||||
style.textContent = cssRule;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Mark icon as loaded
|
||||
loadedIcons.value.push(iconName);
|
||||
|
||||
console.log(`Icon loaded: ${iconName}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load multiple icons
|
||||
* @param {Array} iconNames - Array of icon names to load
|
||||
* @param {Object} options - Icon options (opsz, wght, FILL, GRAD)
|
||||
*/
|
||||
const loadIcons = (iconNames, options = {}) => {
|
||||
if (!Array.isArray(iconNames)) {
|
||||
console.warn('iconNames must be an array');
|
||||
return;
|
||||
}
|
||||
|
||||
iconNames.forEach(iconName => {
|
||||
loadIcon(iconName, options);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure icons are loaded (alias for loadIcons for backward compatibility)
|
||||
* @param {Object} options - Icon options (opsz, wght, FILL, GRAD)
|
||||
* @param {Array} iconNames - Array of icon names to load
|
||||
*/
|
||||
const ensureIconsLoaded = (options = {}, iconNames = []) => {
|
||||
if (iconNames && iconNames.length > 0) {
|
||||
loadIcons(iconNames, options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Watch a reactive property for icon changes and automatically load icons
|
||||
* @param {Ref|Function} iconSource - Reactive source that contains icon name(s)
|
||||
* @param {Object} options - Icon options
|
||||
*/
|
||||
const watchIcon = (iconSource, options = {}) => {
|
||||
watch(
|
||||
iconSource,
|
||||
(newIcon) => {
|
||||
if (newIcon) {
|
||||
if (Array.isArray(newIcon)) {
|
||||
loadIcons(newIcon, options);
|
||||
} else {
|
||||
loadIcon(newIcon, options);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Watch formData for icon changes (common pattern in Vue components)
|
||||
* @param {Ref} formData - Reactive formData object
|
||||
* @param {Object} options - Icon options
|
||||
*/
|
||||
const watchFormDataIcon = (formData, options = {}) => {
|
||||
watch(
|
||||
() => formData.value?.icon,
|
||||
(newIcon) => {
|
||||
if (newIcon) {
|
||||
loadIcon(newIcon, options);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Preload common icons used throughout the application
|
||||
* @param {Array} commonIcons - Array of commonly used icon names
|
||||
* @param {Object} options - Icon options
|
||||
*/
|
||||
const preloadCommonIcons = (commonIcons = [], options = {}) => {
|
||||
const defaultCommonIcons = [
|
||||
'send', 'attach_file', 'mic', 'more_vert', 'close', 'check',
|
||||
'error', 'warning', 'info', 'expand_more', 'expand_less'
|
||||
];
|
||||
|
||||
const iconsToLoad = commonIcons.length > 0 ? commonIcons : defaultCommonIcons;
|
||||
loadIcons(iconsToLoad, options);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
isIconManagerReady,
|
||||
|
||||
// Methods
|
||||
loadIcon,
|
||||
loadIcons,
|
||||
ensureIconsLoaded,
|
||||
|
||||
// Watchers
|
||||
watchIcon,
|
||||
watchFormDataIcon,
|
||||
|
||||
// Utilities
|
||||
preloadCommonIcons
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified composable for basic icon loading
|
||||
* Use this when you only need basic icon loading functionality
|
||||
*/
|
||||
export function useIcon(iconName, options = {}) {
|
||||
const { loadIcon, isIconManagerReady } = useIconManager();
|
||||
|
||||
onMounted(() => {
|
||||
if (iconName) {
|
||||
loadIcon(iconName, options);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
loadIcon,
|
||||
isIconManagerReady
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for form-related icon management
|
||||
* Automatically handles formData.icon watching
|
||||
*/
|
||||
export function useFormIcon(formData, options = {}) {
|
||||
const { watchFormDataIcon, loadIcon, isIconManagerReady } = useIconManager();
|
||||
|
||||
// Automatically watch formData for icon changes
|
||||
watchFormDataIcon(formData, options);
|
||||
|
||||
return {
|
||||
loadIcon,
|
||||
isIconManagerReady
|
||||
};
|
||||
}
|
||||
233
eveai_chat_client/static/assets/js/composables/useTranslation.js
Normal file
233
eveai_chat_client/static/assets/js/composables/useTranslation.js
Normal file
@@ -0,0 +1,233 @@
|
||||
// eveai_chat_client/static/assets/js/composables/useTranslation.js
|
||||
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Vue 3 Composable for translation management
|
||||
* Provides direct backend API communication for translations
|
||||
*/
|
||||
export function useTranslation() {
|
||||
const isTranslationReady = ref(false);
|
||||
const currentLanguage = ref('nl');
|
||||
const isTranslating = ref(false);
|
||||
const lastError = ref(null);
|
||||
|
||||
// Check if translation system is available
|
||||
const checkTranslationReady = () => {
|
||||
// Translation is altijd ready omdat we de backend API gebruiken
|
||||
// Controleer alleen of we in een browser environment zijn
|
||||
if (typeof window !== 'undefined' && typeof fetch !== 'undefined') {
|
||||
isTranslationReady.value = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Eenvoudige check - geen retry mechanism nodig
|
||||
checkTranslationReady();
|
||||
});
|
||||
|
||||
/**
|
||||
* Translate text to target language
|
||||
* @param {string} text - Text to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {string|null} sourceLang - Source language code (optional)
|
||||
* @param {string|null} context - Translation context (optional)
|
||||
* @param {string} apiPrefix - API prefix for tenant routing
|
||||
* @returns {Promise<object>} Translation result
|
||||
*/
|
||||
const translate = async (text, targetLang, sourceLang = null, context = null, apiPrefix = '') => {
|
||||
if (!text || !text.trim()) {
|
||||
const error = new Error('No text provided for translation');
|
||||
lastError.value = error;
|
||||
throw error;
|
||||
}
|
||||
|
||||
isTranslating.value = true;
|
||||
lastError.value = null;
|
||||
|
||||
try {
|
||||
// Bepaal de juiste API URL
|
||||
const baseUrl = apiPrefix || window.chatConfig?.apiPrefix || '';
|
||||
const apiUrl = `${baseUrl}/api/translate`;
|
||||
|
||||
// Maak de request payload
|
||||
const payload = {
|
||||
text: text,
|
||||
target_lang: targetLang
|
||||
};
|
||||
|
||||
// Voeg optionele parameters toe
|
||||
if (sourceLang) payload.source_lang = sourceLang;
|
||||
if (context) payload.context = context;
|
||||
|
||||
// Maak de HTTP request
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin', // Voor sessie cookies
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update current language if translation was successful
|
||||
if (result.success) {
|
||||
currentLanguage.value = targetLang;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Translation error in composable:', error);
|
||||
lastError.value = error;
|
||||
throw error;
|
||||
} finally {
|
||||
isTranslating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate text with automatic error handling and loading state
|
||||
* @param {string} text - Text to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {Object} options - Translation options
|
||||
* @returns {Promise<string|null>} Translated text or null on error
|
||||
*/
|
||||
const translateSafe = async (text, targetLang, options = {}) => {
|
||||
const {
|
||||
sourceLang = null,
|
||||
context = null,
|
||||
apiPrefix = '',
|
||||
fallbackText = text
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const result = await translate(text, targetLang, sourceLang, context, apiPrefix);
|
||||
return result.success ? result.translated_text : fallbackText;
|
||||
} catch (error) {
|
||||
console.warn('Safe translation failed, using fallback:', error.message);
|
||||
return fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Batch translate multiple texts
|
||||
* @param {Array<string>} texts - Array of texts to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {Object} options - Translation options
|
||||
* @returns {Promise<Array<string>>} Array of translated texts
|
||||
*/
|
||||
const translateBatch = async (texts, targetLang, options = {}) => {
|
||||
const results = await Promise.allSettled(
|
||||
texts.map(text => translateSafe(text, targetLang, options))
|
||||
);
|
||||
|
||||
return results.map((result, index) =>
|
||||
result.status === 'fulfilled' ? result.value : texts[index]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current language from chatConfig or fallback
|
||||
*/
|
||||
const getCurrentLanguage = () => {
|
||||
return window.chatConfig?.language || currentLanguage.value || 'nl';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get API prefix from chatConfig or fallback
|
||||
*/
|
||||
const getApiPrefix = () => {
|
||||
return window.chatConfig?.apiPrefix || '';
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
isTranslationReady,
|
||||
currentLanguage: computed(() => getCurrentLanguage()),
|
||||
isTranslating,
|
||||
lastError,
|
||||
|
||||
// Methods
|
||||
translate,
|
||||
translateSafe,
|
||||
translateBatch,
|
||||
|
||||
// Utilities
|
||||
getCurrentLanguage,
|
||||
getApiPrefix
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified composable for basic translation needs
|
||||
* Use this when you only need simple text translation
|
||||
*/
|
||||
export function useTranslationClient() {
|
||||
const { translate, translateSafe, isTranslationReady, isTranslating, lastError } = useTranslation();
|
||||
|
||||
return {
|
||||
translate,
|
||||
translateSafe,
|
||||
isTranslationReady,
|
||||
isTranslating,
|
||||
lastError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive text translation
|
||||
* Automatically translates text when language changes
|
||||
*/
|
||||
export function useReactiveTranslation(text, options = {}) {
|
||||
const { translateSafe, currentLanguage } = useTranslation();
|
||||
const translatedText = ref(text);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const {
|
||||
context = null,
|
||||
sourceLang = null,
|
||||
autoTranslate = true
|
||||
} = options;
|
||||
|
||||
// Watch for language changes and auto-translate
|
||||
if (autoTranslate) {
|
||||
// We'll implement this when we have proper reactivity setup
|
||||
// For now, provide manual translation method
|
||||
}
|
||||
|
||||
const updateTranslation = async (newLanguage = null) => {
|
||||
const targetLang = newLanguage || currentLanguage.value;
|
||||
|
||||
if (!text || targetLang === sourceLang) {
|
||||
translatedText.value = text;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await translateSafe(text, targetLang, {
|
||||
sourceLang,
|
||||
context,
|
||||
apiPrefix: window.chatConfig?.apiPrefix || ''
|
||||
});
|
||||
translatedText.value = result;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
translatedText,
|
||||
isLoading,
|
||||
updateTranslation
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// eveai_chat_client/static/assets/js/composables/useTranslation.js
|
||||
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Vue 3 Composable for translation management
|
||||
* Provides modern alternative to window.TranslationClient
|
||||
*/
|
||||
export function useTranslation() {
|
||||
const isTranslationReady = ref(false);
|
||||
const currentLanguage = ref('nl');
|
||||
const isTranslating = ref(false);
|
||||
const lastError = ref(null);
|
||||
|
||||
// Check if translation system is available with retry mechanism
|
||||
const checkTranslationReady = () => {
|
||||
if (window.TranslationClient && typeof window.TranslationClient.translate === 'function') {
|
||||
isTranslationReady.value = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Initial check
|
||||
if (checkTranslationReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retry mechanism - wait for TranslationClient to become available
|
||||
let retryCount = 0;
|
||||
const maxRetries = 10;
|
||||
const retryInterval = 100; // 100ms
|
||||
|
||||
const retryCheck = () => {
|
||||
if (checkTranslationReady()) {
|
||||
return; // Success!
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount < maxRetries) {
|
||||
setTimeout(retryCheck, retryInterval);
|
||||
} else {
|
||||
console.warn('TranslationClient is not available after retries');
|
||||
isTranslationReady.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Start retry process
|
||||
setTimeout(retryCheck, retryInterval);
|
||||
});
|
||||
|
||||
/**
|
||||
* Translate text to target language
|
||||
* @param {string} text - Text to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {string|null} sourceLang - Source language code (optional)
|
||||
* @param {string|null} context - Translation context (optional)
|
||||
* @param {string} apiPrefix - API prefix for tenant routing
|
||||
* @returns {Promise<object>} Translation result
|
||||
*/
|
||||
const translate = async (text, targetLang, sourceLang = null, context = null, apiPrefix = '') => {
|
||||
if (!isTranslationReady.value || !window.TranslationClient) {
|
||||
const error = new Error('Translation system not ready');
|
||||
lastError.value = error;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!text || !text.trim()) {
|
||||
const error = new Error('No text provided for translation');
|
||||
lastError.value = error;
|
||||
throw error;
|
||||
}
|
||||
|
||||
isTranslating.value = true;
|
||||
lastError.value = null;
|
||||
|
||||
try {
|
||||
const result = await window.TranslationClient.translate(
|
||||
text,
|
||||
targetLang,
|
||||
sourceLang,
|
||||
context,
|
||||
apiPrefix
|
||||
);
|
||||
|
||||
// Update current language if translation was successful
|
||||
if (result.success) {
|
||||
currentLanguage.value = targetLang;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Translation error in composable:', error);
|
||||
lastError.value = error;
|
||||
throw error;
|
||||
} finally {
|
||||
isTranslating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate text with automatic error handling and loading state
|
||||
* @param {string} text - Text to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {Object} options - Translation options
|
||||
* @returns {Promise<string|null>} Translated text or null on error
|
||||
*/
|
||||
const translateSafe = async (text, targetLang, options = {}) => {
|
||||
const {
|
||||
sourceLang = null,
|
||||
context = null,
|
||||
apiPrefix = '',
|
||||
fallbackText = text
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const result = await translate(text, targetLang, sourceLang, context, apiPrefix);
|
||||
return result.success ? result.translated_text : fallbackText;
|
||||
} catch (error) {
|
||||
console.warn('Safe translation failed, using fallback:', error.message);
|
||||
return fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Batch translate multiple texts
|
||||
* @param {Array<string>} texts - Array of texts to translate
|
||||
* @param {string} targetLang - Target language code
|
||||
* @param {Object} options - Translation options
|
||||
* @returns {Promise<Array<string>>} Array of translated texts
|
||||
*/
|
||||
const translateBatch = async (texts, targetLang, options = {}) => {
|
||||
const results = await Promise.allSettled(
|
||||
texts.map(text => translateSafe(text, targetLang, options))
|
||||
);
|
||||
|
||||
return results.map((result, index) =>
|
||||
result.status === 'fulfilled' ? result.value : texts[index]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current language from chatConfig or fallback
|
||||
*/
|
||||
const getCurrentLanguage = () => {
|
||||
return window.chatConfig?.language || currentLanguage.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get API prefix from chatConfig or fallback
|
||||
*/
|
||||
const getApiPrefix = () => {
|
||||
return window.chatConfig?.apiPrefix || '';
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
isTranslationReady,
|
||||
currentLanguage: computed(() => getCurrentLanguage()),
|
||||
isTranslating,
|
||||
lastError,
|
||||
|
||||
// Methods
|
||||
translate,
|
||||
translateSafe,
|
||||
translateBatch,
|
||||
|
||||
// Utilities
|
||||
getCurrentLanguage,
|
||||
getApiPrefix
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified composable for basic translation needs
|
||||
* Use this when you only need simple text translation
|
||||
*/
|
||||
export function useTranslationClient() {
|
||||
const { translate, translateSafe, isTranslationReady, isTranslating, lastError } = useTranslation();
|
||||
|
||||
return {
|
||||
translate,
|
||||
translateSafe,
|
||||
isTranslationReady,
|
||||
isTranslating,
|
||||
lastError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for reactive text translation
|
||||
* Automatically translates text when language changes
|
||||
*/
|
||||
export function useReactiveTranslation(text, options = {}) {
|
||||
const { translateSafe, currentLanguage } = useTranslation();
|
||||
const translatedText = ref(text);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const {
|
||||
context = null,
|
||||
sourceLang = null,
|
||||
autoTranslate = true
|
||||
} = options;
|
||||
|
||||
// Watch for language changes and auto-translate
|
||||
if (autoTranslate) {
|
||||
// We'll implement this when we have proper reactivity setup
|
||||
// For now, provide manual translation method
|
||||
}
|
||||
|
||||
const updateTranslation = async (newLanguage = null) => {
|
||||
const targetLang = newLanguage || currentLanguage.value;
|
||||
|
||||
if (!text || targetLang === sourceLang) {
|
||||
translatedText.value = text;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await translateSafe(text, targetLang, {
|
||||
sourceLang,
|
||||
context,
|
||||
apiPrefix: window.chatConfig?.apiPrefix || ''
|
||||
});
|
||||
translatedText.value = result;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
translatedText,
|
||||
isLoading,
|
||||
updateTranslation
|
||||
};
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// static/js/iconManager.js
|
||||
|
||||
/**
|
||||
* Een eenvoudige standalone icon manager voor Material Symbols Outlined
|
||||
* Deze kan direct worden gebruikt zonder Vue
|
||||
*/
|
||||
window.iconManager = {
|
||||
loadedIcons: [],
|
||||
|
||||
/**
|
||||
* Laadt een Material Symbols Outlined icoon als het nog niet is geladen
|
||||
* @param {string} iconName - Naam van het icoon
|
||||
* @param {Object} options - Opties voor het icoon (opsz, wght, FILL, GRAD)
|
||||
*/
|
||||
loadIcon: function(iconName, options = {}) {
|
||||
if (!iconName) return;
|
||||
|
||||
if (this.loadedIcons.includes(iconName)) {
|
||||
return; // Icoon is al geladen
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
opsz: 24,
|
||||
wght: 400,
|
||||
FILL: 0,
|
||||
GRAD: 0
|
||||
};
|
||||
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
// Genereer unieke ID voor het stylesheet element
|
||||
const styleId = `material-symbols-${iconName}`;
|
||||
|
||||
// Controleer of het stylesheet al bestaat
|
||||
if (!document.getElementById(styleId)) {
|
||||
const link = document.createElement('link');
|
||||
link.id = styleId;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = `https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@${opts.opsz},${opts.wght},${opts.FILL},${opts.GRAD}&icon_names=${iconName}`;
|
||||
document.head.appendChild(link);
|
||||
console.log(`Material Symbol geladen: ${iconName}`);
|
||||
|
||||
this.loadedIcons.push(iconName);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Laadt een set van Material Symbols Outlined iconen
|
||||
* @param {Array} iconNames - Array met icoonnamen
|
||||
* @param {Object} options - Opties voor de iconen
|
||||
*/
|
||||
loadIcons: function(iconNames, options = {}) {
|
||||
if (!iconNames || !Array.isArray(iconNames) || iconNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter alleen iconen die nog niet zijn geladen
|
||||
const newIcons = iconNames.filter(icon => !this.loadedIcons.includes(icon));
|
||||
|
||||
if (newIcons.length === 0) {
|
||||
return; // Alle iconen zijn al geladen
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
opsz: 24,
|
||||
wght: 400,
|
||||
FILL: 0,
|
||||
GRAD: 0
|
||||
};
|
||||
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
// Genereer unieke ID voor het stylesheet element
|
||||
const styleId = `material-symbols-set-${newIcons.join('-')}`;
|
||||
|
||||
// Controleer of het stylesheet al bestaat
|
||||
if (!document.getElementById(styleId)) {
|
||||
const link = document.createElement('link');
|
||||
link.id = styleId;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = `https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@${opts.opsz},${opts.wght},${opts.FILL},${opts.GRAD}&icon_names=${newIcons.join(',')}`;
|
||||
document.head.appendChild(link);
|
||||
console.log(`Material Symbols geladen: ${newIcons.join(', ')}`);
|
||||
|
||||
// Voeg de nieuwe iconen toe aan de geladen lijst
|
||||
this.loadedIcons.push(...newIcons);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export de iconManager functie om te gebruiken in Vue componenten
|
||||
// Dit vervangt de complexe injectie in het DynamicForm component
|
||||
export { iconManager as default };
|
||||
|
||||
// We exporteren iconManager als default, maar houden ook de window.iconManager beschikbaar
|
||||
// voor backwards compatibility met bestaande code
|
||||
|
||||
// Maak een Vue mixin die iconManager toevoegt aan elk component dat het nodig heeft
|
||||
export const IconManagerMixin = {
|
||||
created() {
|
||||
// Check of er een formData.icon property is
|
||||
if (this.formData && this.formData.icon) {
|
||||
window.iconManager.loadIcon(this.formData.icon);
|
||||
}
|
||||
},
|
||||
|
||||
// Watch voor formData.icon veranderingen
|
||||
watch: {
|
||||
'formData.icon': function(newIcon) {
|
||||
if (newIcon) {
|
||||
window.iconManager.loadIcon(newIcon);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Methode om toe te voegen aan componenten
|
||||
methods: {
|
||||
loadIcon(iconName, options) {
|
||||
window.iconManager.loadIcon(iconName, options);
|
||||
},
|
||||
|
||||
loadIcons(iconNames, options) {
|
||||
window.iconManager.loadIcons(iconNames, options);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// We hoeven niet langer DynamicForm te manipuleren
|
||||
// omdat Vue componenten nu de IconManagerMixin kunnen gebruiken
|
||||
console.log('IconManager en IconManagerMixin zijn beschikbaar voor componenten');
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* 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}/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');
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
eveai_chat_client/static/assets/vue-components/SideBar.vue
Normal file
111
eveai_chat_client/static/assets/vue-components/SideBar.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -27,17 +27,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app-container" data-vue-app="true">
|
||||
<!-- Left sidebar - never changes -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<img src="{{ tenant_make.logo_url|default('') }}" alt="{{ tenant_make.name|default('Logo') }}">
|
||||
</div>
|
||||
<div class="sidebar-make-name">
|
||||
{{ tenant_make.name|default('') }}
|
||||
</div>
|
||||
<div id="language-selector-container"></div>
|
||||
<div class="sidebar-explanation" id="sidebar-explanation"></div>
|
||||
</div>
|
||||
<!-- Left sidebar - Vue component container -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<!-- Right content area - contains the chat client -->
|
||||
<div class="content-area">
|
||||
|
||||
Reference in New Issue
Block a user