- introductie van vue files - bijna werkende versie van eveai_chat_client.
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
// Import all components via barrel export
|
||||
import { TypingIndicator, FormField, DynamicForm, ChatMessage, MessageHistory, ProgressTracker, LanguageSelector, ChatInput } from './components/index.js';
|
||||
// Import all components as Vue SFCs
|
||||
import TypingIndicator from '../vue-components/TypingIndicator.vue';
|
||||
import FormField from '../vue-components/FormField.vue';
|
||||
import DynamicForm from '../vue-components/DynamicForm.vue';
|
||||
import ChatMessage from '../vue-components/ChatMessage.vue';
|
||||
import MessageHistory from '../vue-components/MessageHistory.vue';
|
||||
import ProgressTracker from '../vue-components/ProgressTracker.vue';
|
||||
import LanguageSelector from '../vue-components/LanguageSelector.vue';
|
||||
import ChatInput from '../vue-components/ChatInput.vue';
|
||||
|
||||
// Main Chat Application
|
||||
// Main Chat Application - geëxporteerd als module
|
||||
|
||||
@@ -12,72 +12,6 @@ export const ChatInput = {
|
||||
components: {
|
||||
'dynamic-form': DynamicForm
|
||||
},
|
||||
// Static method for direct rendering
|
||||
renderComponent(container, props, app) {
|
||||
console.log('🔍 [DEBUG] ChatInput.renderComponent() aangeroepen');
|
||||
console.log('🔍 [DEBUG] ChatInput container:', container);
|
||||
console.log('🔍 [DEBUG] ChatInput props:', props);
|
||||
console.log('🔍 [DEBUG] ChatInput app:', app);
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor ChatInput');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Controleer de globale dependencies
|
||||
console.log('🔍 [DEBUG] Global dependencies check:');
|
||||
console.log('- window.Vue:', typeof window.Vue);
|
||||
if (window.Vue) {
|
||||
console.log('- window.Vue.createApp:', typeof window.Vue.createApp);
|
||||
console.log('- window.Vue.version:', window.Vue.version);
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] ChatInput container gevonden, Vue app aan het initialiseren');
|
||||
|
||||
try {
|
||||
// We controleren het app object
|
||||
if (!app) {
|
||||
console.error('🚨 [ERROR] Geen Vue app object ontvangen');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check of we een correcte Vue app hebben of we moeten er een maken
|
||||
if (typeof app.mount !== 'function') {
|
||||
console.log('🔍 [DEBUG] Ontvangen app heeft geen mount functie, dit is mogelijk een config object');
|
||||
// Controleer of window.Vue beschikbaar is
|
||||
if (!window.Vue || typeof window.Vue.createApp !== 'function') {
|
||||
console.error('🚨 [ERROR] window.Vue.createApp is niet beschikbaar');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Maak een nieuwe Vue app met het ChatInput component en de props
|
||||
console.log('🔍 [DEBUG] Nieuwe Vue app aanmaken met ChatInput component');
|
||||
try {
|
||||
app = window.Vue.createApp(ChatInput, props);
|
||||
console.log('🔍 [DEBUG] Nieuwe app aangemaakt:', app);
|
||||
} catch (createError) {
|
||||
console.error('🚨 [ERROR] Fout bij aanmaken Vue app:', createError);
|
||||
// Probeer een alternatieve aanpak zonder importreferenties
|
||||
console.log('🔍 [DEBUG] Alternatieve aanpak proberen...');
|
||||
const componentCopy = JSON.parse(JSON.stringify(ChatInput));
|
||||
app = window.Vue.createApp(componentCopy, props);
|
||||
}
|
||||
}
|
||||
|
||||
// Stel een fallback DOM in voor het geval mounten mislukt
|
||||
container.innerHTML = `<div class="chat-input-loading">Chat input laden...</div>`;
|
||||
|
||||
// Nu kunnen we de app mounten
|
||||
console.log('🔍 [DEBUG] App.mount aanroepen op container');
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] ChatInput component succesvol gemount, instance:', instance);
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten ChatInput component:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
// Gebruik de IconManagerMixin om automatisch iconen te laden
|
||||
mixins: [IconManagerMixin],
|
||||
created() {
|
||||
|
||||
@@ -7,52 +7,6 @@ import { ProgressTracker } from './ProgressTracker.js';
|
||||
|
||||
export const ChatMessage = {
|
||||
name: 'ChatMessage',
|
||||
// Static method for direct rendering
|
||||
renderComponent(container, props, app) {
|
||||
console.log('🔍 [DEBUG] ChatMessage.renderComponent() aangeroepen');
|
||||
console.log('🔍 [DEBUG] ChatMessage container:', container);
|
||||
console.log('🔍 [DEBUG] ChatMessage props:', props);
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor ChatMessage');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] ChatMessage container gevonden, Vue app aan het initialiseren');
|
||||
|
||||
try {
|
||||
// We controleren het app object
|
||||
if (!app) {
|
||||
console.error('🚨 [ERROR] Geen Vue app object ontvangen');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check of we een correcte Vue app hebben of we moeten er een maken
|
||||
if (typeof app.mount !== 'function') {
|
||||
console.log('🔍 [DEBUG] Ontvangen app heeft geen mount functie, dit is mogelijk een config object');
|
||||
// Controleer of window.Vue beschikbaar is
|
||||
if (!window.Vue || typeof window.Vue.createApp !== 'function') {
|
||||
console.error('🚨 [ERROR] window.Vue.createApp is niet beschikbaar');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Maak een nieuwe Vue app met het ChatMessage component en de props
|
||||
console.log('🔍 [DEBUG] Nieuwe Vue app aanmaken met ChatMessage component');
|
||||
app = window.Vue.createApp(ChatMessage, props);
|
||||
console.log('🔍 [DEBUG] Nieuwe app aangemaakt:', app);
|
||||
}
|
||||
|
||||
// Nu kunnen we de app mounten
|
||||
console.log('🔍 [DEBUG] App.mount aanroepen op container');
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] ChatMessage component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten ChatMessage component:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'dynamic-form': DynamicForm,
|
||||
'progress-tracker': ProgressTracker
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
export const DynamicForm = {
|
||||
name: 'DynamicForm',
|
||||
// Static method for direct rendering
|
||||
renderComponent(container, props, app) {
|
||||
console.log('🔍 [DEBUG] DynamicForm.renderComponent() aangeroepen');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor DynamicForm');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] DynamicForm container gevonden, Vue app aanmaken');
|
||||
|
||||
try {
|
||||
// Maak een nieuwe Vue app instantie met dit component
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] DynamicForm component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten DynamicForm component:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Zorg ervoor dat het icoon geladen wordt als iconManager beschikbaar is
|
||||
if (window.iconManager && this.formData && this.formData.icon) {
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
export const FormField = {
|
||||
name: 'FormField',
|
||||
// Static method for direct rendering
|
||||
renderComponent(container, props, app) {
|
||||
console.log('🔍 [DEBUG] FormField.renderComponent() aangeroepen');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor FormField');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] FormField container gevonden, Vue app aanmaken');
|
||||
|
||||
try {
|
||||
// Maak een nieuwe Vue app instantie met dit component
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] FormField component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten FormField component:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
|
||||
@@ -2,52 +2,6 @@
|
||||
|
||||
export const FormMessage = {
|
||||
name: 'FormMessage',
|
||||
// Static method for direct rendering
|
||||
renderComponent(container, props, app) {
|
||||
console.log('🔍 [DEBUG] FormMessage.renderComponent() aangeroepen');
|
||||
console.log('🔍 [DEBUG] FormMessage container:', container);
|
||||
console.log('🔍 [DEBUG] FormMessage props:', props);
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor FormMessage');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] FormMessage container gevonden, Vue app aan het initialiseren');
|
||||
|
||||
try {
|
||||
// We controleren het app object
|
||||
if (!app) {
|
||||
console.error('🚨 [ERROR] Geen Vue app object ontvangen');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check of we een correcte Vue app hebben of we moeten er een maken
|
||||
if (typeof app.mount !== 'function') {
|
||||
console.log('🔍 [DEBUG] Ontvangen app heeft geen mount functie, dit is mogelijk een config object');
|
||||
// Controleer of window.Vue beschikbaar is
|
||||
if (!window.Vue || typeof window.Vue.createApp !== 'function') {
|
||||
console.error('🚨 [ERROR] window.Vue.createApp is niet beschikbaar');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Maak een nieuwe Vue app met het FormMessage component en de props
|
||||
console.log('🔍 [DEBUG] Nieuwe Vue app aanmaken met FormMessage component');
|
||||
app = window.Vue.createApp(FormMessage, props);
|
||||
console.log('🔍 [DEBUG] Nieuwe app aangemaakt:', app);
|
||||
}
|
||||
|
||||
// Nu kunnen we de app mounten
|
||||
console.log('🔍 [DEBUG] App.mount aanroepen op container');
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] FormMessage component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten FormMessage component:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
|
||||
@@ -26,16 +26,13 @@ export const LanguageSelector = {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// console.log('🔍 [DEBUG] LanguageSelector mounted');
|
||||
// console.log('🔍 [DEBUG] Props:', {
|
||||
// initialLanguage: this.initialLanguage,
|
||||
// currentLanguage: this.currentLanguage,
|
||||
// supportedLanguageDetails: this.supportedLanguageDetails,
|
||||
// allowedLanguages: this.allowedLanguages
|
||||
// });
|
||||
|
||||
// Render the component
|
||||
this.renderComponent();
|
||||
console.log('🔍 [DEBUG] LanguageSelector mounted with Vue template');
|
||||
console.log('🔍 [DEBUG] Props:', {
|
||||
initialLanguage: this.initialLanguage,
|
||||
currentLanguage: this.currentLanguage,
|
||||
supportedLanguageDetails: this.supportedLanguageDetails,
|
||||
allowedLanguages: this.allowedLanguages
|
||||
});
|
||||
|
||||
// Emit initial language
|
||||
this.$emit('language-changed', this.selectedLanguage);
|
||||
@@ -102,48 +99,9 @@ export const LanguageSelector = {
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
renderComponent() {
|
||||
// We gaan direct de container manipuleren
|
||||
const container = document.getElementById('language-selector-container');
|
||||
if (!container) {
|
||||
console.error('Container niet gevonden voor LanguageSelector');
|
||||
return;
|
||||
}
|
||||
|
||||
const availableLanguages = this.getAvailableLanguages();
|
||||
// console.log('🔍 [DEBUG] Available languages:', availableLanguages);
|
||||
|
||||
const optionsHtml = availableLanguages.map(lang =>
|
||||
`<option value="${lang.code}" ${lang.code === this.selectedLanguage ? 'selected' : ''}>${lang.flag} ${lang.name}</option>`
|
||||
).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="language-selector">
|
||||
<label for="language-select">Taal / Language:</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="language-select" class="language-select">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listener
|
||||
const selectElement = container.querySelector('#language-select');
|
||||
if (selectElement) {
|
||||
selectElement.addEventListener('change', (e) => {
|
||||
this.changeLanguage(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// console.log('🔍 [DEBUG] Component rendered successfully');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Stap 1: Ondersteun zowel template-based rendering als manual rendering
|
||||
// Door beide methoden te ondersteunen, werkt het component in beide scenario's
|
||||
template: `
|
||||
<div class="language-selector">
|
||||
<label for="language-select">Taal / Language:</label>
|
||||
@@ -153,10 +111,5 @@ export const LanguageSelector = {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
// Minimale render functie als fallback
|
||||
render() {
|
||||
return document.createElement('div');
|
||||
}
|
||||
`
|
||||
};
|
||||
@@ -227,26 +227,4 @@
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// Statische renderComponent methode voor het MessageHistory object
|
||||
MessageHistory.renderComponent = function(container, props, app) {
|
||||
console.log('🔍 [DEBUG] MessageHistory.renderComponent() aangeroepen als statische methode');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor MessageHistory');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] MessageHistory container gevonden, Vue app aanmaken');
|
||||
|
||||
try {
|
||||
// Maak een nieuwe Vue app instantie met dit component
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] MessageHistory component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten MessageHistory component:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -308,26 +308,4 @@ export const ProgressTracker = {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// Voeg de renderComponent toe als statische methode op het ProgressTracker object
|
||||
ProgressTracker.renderComponent = function(container, props, app) {
|
||||
console.log('🔍 [DEBUG] ProgressTracker.renderComponent() aangeroepen');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor ProgressTracker');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] ProgressTracker container gevonden, Vue app aanmaken');
|
||||
|
||||
try {
|
||||
// Maak een nieuwe Vue app instantie met dit component
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] ProgressTracker component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten ProgressTracker component:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -21,26 +21,4 @@ export const TypingIndicator = {
|
||||
<div v-if="showText" class="typing-text">{{ text }}</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// Voeg statische renderComponent methode toe aan het TypingIndicator object
|
||||
TypingIndicator.renderComponent = function(container, props, app) {
|
||||
console.log('🔍 [DEBUG] TypingIndicator.renderComponent() aangeroepen als statische methode');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container element niet gevonden voor TypingIndicator');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('🔍 [DEBUG] TypingIndicator container gevonden, Vue app aanmaken');
|
||||
|
||||
try {
|
||||
// Maak een nieuwe Vue app instantie met dit component
|
||||
const instance = app.mount(container);
|
||||
console.log('🔍 [DEBUG] TypingIndicator component succesvol gemount');
|
||||
return instance;
|
||||
} catch (error) {
|
||||
console.error('🚨 [ERROR] Fout bij mounten TypingIndicator component:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
512
eveai_chat_client/static/assets/vue-components/ChatInput.vue
Normal file
512
eveai_chat_client/static/assets/vue-components/ChatInput.vue
Normal file
@@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<div class="chat-input-container">
|
||||
<!-- Material Icons worden nu globaal geladen in scripts.html -->
|
||||
<!-- Dynamisch formulier container -->
|
||||
<div v-if="formData" class="dynamic-form-container">
|
||||
<!-- De titel wordt in DynamicForm weergegeven en niet hier om dubbele titels te voorkomen -->
|
||||
<div v-if="!formData.fields" style="color: red; padding: 10px;">Fout: Geen velden gevonden in formulier</div>
|
||||
<dynamic-form
|
||||
v-if="formData && formData.fields"
|
||||
:form-data="formData"
|
||||
:form-values="formValues"
|
||||
:is-submitting="isLoading"
|
||||
:hide-actions="true"
|
||||
@update:form-values="updateFormValues"
|
||||
></dynamic-form>
|
||||
|
||||
<!-- Geen extra knoppen meer onder het formulier, alles gaat via de hoofdverzendknop -->
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<!-- Main input area -->
|
||||
<div class="input-main">
|
||||
<textarea
|
||||
ref="messageInput"
|
||||
v-model="localMessage"
|
||||
@keydown="handleKeydown"
|
||||
:placeholder="translatedPlaceholder"
|
||||
rows="1"
|
||||
:disabled="isLoading"
|
||||
:maxlength="maxLength"
|
||||
class="message-input"
|
||||
:class="{ 'over-limit': isOverLimit }"
|
||||
></textarea>
|
||||
|
||||
<!-- Character counter -->
|
||||
<div v-if="maxLength" class="character-counter" :class="{ 'over-limit': isOverLimit }">
|
||||
{{ characterCount }}/{{ maxLength }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input actions -->
|
||||
<div class="input-actions">
|
||||
|
||||
<!-- Universele verzendknop (voor zowel berichten als formulieren) -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="send-btn"
|
||||
:class="{ 'form-mode': formData }"
|
||||
:disabled="!canSend"
|
||||
:title="formData ? 'Verstuur formulier' : 'Verstuur bericht'"
|
||||
>
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Importeer de benodigde componenten
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
import { IconManagerMixin } from '../js/iconManager.js';
|
||||
|
||||
export default {
|
||||
name: 'ChatInput',
|
||||
components: {
|
||||
'dynamic-form': DynamicForm
|
||||
},
|
||||
// Gebruik de IconManagerMixin om automatisch iconen te laden
|
||||
mixins: [IconManagerMixin],
|
||||
props: {
|
||||
currentMessage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Typ je bericht hier... - Enter om te verzenden, Shift+Enter voor nieuwe regel'
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 2000
|
||||
},
|
||||
formData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
emits: ['send-message', 'update-message', 'submit-form'],
|
||||
data() {
|
||||
return {
|
||||
localMessage: this.currentMessage,
|
||||
formValues: {},
|
||||
translatedPlaceholder: this.placeholder,
|
||||
isTranslating: false,
|
||||
languageChangeHandler: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
characterCount() {
|
||||
return this.localMessage.length;
|
||||
},
|
||||
|
||||
isOverLimit() {
|
||||
return this.characterCount > this.maxLength;
|
||||
},
|
||||
|
||||
hasFormData() {
|
||||
return this.formData && this.formData.fields &&
|
||||
((Array.isArray(this.formData.fields) && this.formData.fields.length > 0) ||
|
||||
(typeof this.formData.fields === 'object' && Object.keys(this.formData.fields).length > 0));
|
||||
},
|
||||
|
||||
canSend() {
|
||||
const hasValidForm = this.formData && this.validateForm();
|
||||
const hasValidMessage = this.localMessage.trim() && !this.isOverLimit;
|
||||
|
||||
// We kunnen nu verzenden als er een geldig formulier OF een geldig bericht is
|
||||
// Bij een formulier is aanvullende tekst optioneel
|
||||
return (!this.isLoading) && (hasValidForm || hasValidMessage);
|
||||
},
|
||||
|
||||
hasFormDataToSend() {
|
||||
return this.formData && this.validateForm();
|
||||
},
|
||||
|
||||
sendButtonText() {
|
||||
if (this.isLoading) {
|
||||
return 'Verzenden...';
|
||||
}
|
||||
return this.formData ? 'Verstuur formulier' : 'Verstuur bericht';
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
||||
if (!newFormData) {
|
||||
console.log('FormData is null of undefined');
|
||||
this.formValues = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// Controleer of velden aanwezig zijn
|
||||
if (!newFormData.fields) {
|
||||
console.error('FormData bevat geen velden!', newFormData);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Velden in formData:', newFormData.fields);
|
||||
console.log('Aantal velden:', Array.isArray(newFormData.fields)
|
||||
? newFormData.fields.length
|
||||
: Object.keys(newFormData.fields).length);
|
||||
|
||||
// Initialiseer formulierwaarden
|
||||
this.initFormValues();
|
||||
|
||||
// Log de geïnitialiseerde waarden
|
||||
console.log('Formulierwaarden geïnitialiseerd:', this.formValues);
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
currentMessage(newVal) {
|
||||
this.localMessage = newVal;
|
||||
},
|
||||
localMessage(newVal) {
|
||||
this.$emit('update-message', newVal);
|
||||
this.autoResize();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Als er een formData.icon is, wordt deze automatisch geladen via IconManagerMixin
|
||||
// Geen expliciete window.iconManager calls meer nodig
|
||||
|
||||
// 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);
|
||||
},
|
||||
mounted() {
|
||||
this.autoResize();
|
||||
// Debug informatie over formData bij initialisatie
|
||||
console.log('ChatInput mounted, formData:', this.formData);
|
||||
if (this.formData) {
|
||||
console.log('FormData bij mount:', JSON.stringify(this.formData));
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Verwijder event listener bij unmount met de benoemde handler
|
||||
if (this.languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||
}
|
||||
},
|
||||
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);
|
||||
this.formValues = {};
|
||||
|
||||
// Verwerk array van velden
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
this.formData.fields.forEach(field => {
|
||||
const fieldId = field.id || field.name;
|
||||
if (fieldId) {
|
||||
this.formValues[fieldId] = field.default !== undefined ? field.default : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
// Verwerk object van velden
|
||||
else if (typeof this.formData.fields === 'object') {
|
||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||
this.formValues[fieldId] = field.default !== undefined ? field.default : '';
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Initialized form values:', this.formValues);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeydown(event) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.sendMessage();
|
||||
} else if (event.key === 'Escape') {
|
||||
this.localMessage = '';
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
if (!this.canSend) return;
|
||||
|
||||
// Bij een formulier gaan we het formulier en optioneel bericht combineren
|
||||
if (this.formData) {
|
||||
// Valideer het formulier
|
||||
if (this.validateForm()) {
|
||||
// Verstuur het formulier, eventueel met aanvullende tekst
|
||||
this.$emit('submit-form', this.formValues);
|
||||
}
|
||||
} else if (this.localMessage.trim()) {
|
||||
// Verstuur normaal bericht zonder formulier
|
||||
this.$emit('send-message');
|
||||
}
|
||||
},
|
||||
|
||||
getFormValuesForSending() {
|
||||
// Geeft de huidige formulierwaarden terug voor verzending
|
||||
return this.formValues;
|
||||
},
|
||||
|
||||
// Reset het formulier en de waarden
|
||||
resetForm() {
|
||||
this.formValues = {};
|
||||
this.initFormValues();
|
||||
},
|
||||
|
||||
// Annuleer het formulier (wordt momenteel niet gebruikt)
|
||||
cancelForm() {
|
||||
this.formValues = {};
|
||||
// We sturen geen emit meer, maar het kan nuttig zijn om in de toekomst te hebben
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
if (!this.formData || !this.formData.fields) return false;
|
||||
|
||||
// Controleer of alle verplichte velden zijn ingevuld
|
||||
let missingFields = [];
|
||||
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
missingFields = this.formData.fields.filter(field => {
|
||||
if (!field.required) return false;
|
||||
const fieldId = field.id || field.name;
|
||||
const value = this.formValues[fieldId];
|
||||
return value === undefined || value === null || (typeof value === 'string' && !value.trim());
|
||||
});
|
||||
} else {
|
||||
// Voor object-gebaseerde velden
|
||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||
if (field.required) {
|
||||
const value = this.formValues[fieldId];
|
||||
if (value === undefined || value === null || (typeof value === 'string' && !value.trim())) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields.length === 0;
|
||||
},
|
||||
|
||||
autoResize() {
|
||||
this.$nextTick(() => {
|
||||
const textarea = this.$refs.messageInput;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
focusInput() {
|
||||
this.$refs.messageInput?.focus();
|
||||
},
|
||||
|
||||
clearInput() {
|
||||
this.localMessage = '';
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
updateFormValues(newValues) {
|
||||
// Controleer of er daadwerkelijk iets is veranderd om recursieve updates te voorkomen
|
||||
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
|
||||
this.formValues = JSON.parse(JSON.stringify(newValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ChatInput component styling */
|
||||
|
||||
/* Algemene container */
|
||||
.chat-input-container {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Input veld en knoppen */
|
||||
.chat-input {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 10px 40px 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #0084ff;
|
||||
}
|
||||
|
||||
.message-input.over-limit {
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* Character counter */
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.character-counter.over-limit {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
/* Input actions */
|
||||
.input-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Verzendknop */
|
||||
.send-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #0084ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
background-color: #0077e6;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-btn.form-mode {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.send-btn.form-mode:hover {
|
||||
background-color: #43a047;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Formulier in chat input */
|
||||
.dynamic-form-container {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px 15px 5px 15px;
|
||||
position: relative;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
454
eveai_chat_client/static/assets/vue-components/ChatMessage.vue
Normal file
454
eveai_chat_client/static/assets/vue-components/ChatMessage.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div :class="getMessageClass()" :data-message-id="message.id">
|
||||
<!-- Normal text messages -->
|
||||
<template v-if="message.type === 'text'">
|
||||
<div class="message-content" style="width: 100%;">
|
||||
<!-- Voortgangstracker voor AI berichten met task_id - NU BINNEN DE BUBBLE -->
|
||||
<progress-tracker
|
||||
v-if="message.sender === 'ai' && message.taskId"
|
||||
:task-id="message.taskId"
|
||||
:api-prefix="apiPrefix"
|
||||
class="message-progress"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
@specialist-error="handleSpecialistError"
|
||||
></progress-tracker>
|
||||
|
||||
<!-- Form data display if available (alleen in user messages) -->
|
||||
<div v-if="message.formValues && message.sender === 'user'" class="form-display user-form-values">
|
||||
<dynamic-form
|
||||
:form-data="message.formData"
|
||||
:form-values="message.formValues"
|
||||
:read-only="true"
|
||||
hide-actions
|
||||
class="message-form user-form"
|
||||
></dynamic-form>
|
||||
</div>
|
||||
|
||||
<!-- Formulier in AI berichten -->
|
||||
<div v-if="message.formData && message.sender === 'ai'" class="form-display ai-form-values" style="margin-top: 15px;">
|
||||
<!-- Dynamisch toevoegen van Material Symbols Outlined voor iconen -->
|
||||
<table class="form-result-table">
|
||||
<thead v-if="message.formData.title || message.formData.name || message.formData.icon">
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<div class="form-header">
|
||||
<span v-if="message.formData.icon" class="material-symbols-outlined">{{ message.formData.icon }}</span>
|
||||
<span>{{ message.formData.title || message.formData.name }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(field, fieldId) in message.formData.fields" :key="fieldId">
|
||||
<td class="field-label">{{ field.name }}:</td>
|
||||
<td class="field-value">
|
||||
<input
|
||||
v-if="field.type === 'str' || field.type === 'string' || field.type === 'int' || field.type === 'integer' || field.type === 'float'"
|
||||
:type="field.type === 'int' || field.type === 'integer' || field.type === 'float' ? 'number' : 'text'"
|
||||
:placeholder="field.placeholder || ''"
|
||||
class="form-input"
|
||||
>
|
||||
<textarea
|
||||
v-else-if="field.type === 'text'"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:rows="field.rows || 3"
|
||||
class="form-textarea"
|
||||
></textarea>
|
||||
<select
|
||||
v-else-if="field.type === 'enum'"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">Selecteer een optie</option>
|
||||
<option
|
||||
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else-if="field.type === 'boolean'" class="toggle-switch">
|
||||
<input type="checkbox" class="toggle-input">
|
||||
<span class="toggle-slider">
|
||||
<span class="toggle-knob"></span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
:placeholder="field.placeholder || ''"
|
||||
class="form-input"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bericht tekst -->
|
||||
<div v-if="message.content" class="message-text" v-html="formatMessage(message.content)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error messages -->
|
||||
<template v-else-if="message.type === 'error'">
|
||||
<div class="message-content error-content">
|
||||
<div class="form-error">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<button v-if="message.retryable" @click="$emit('retry-message', message.id)" class="retry-btn">
|
||||
Opnieuw proberen
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Other message types -->
|
||||
<template v-else>
|
||||
<div class="message-content">
|
||||
<div class="message-text" v-html="formatMessage(message.content)"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import benodigde componenten
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
import ProgressTracker from './ProgressTracker.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChatMessage',
|
||||
components: {
|
||||
'dynamic-form': DynamicForm,
|
||||
'progress-tracker': ProgressTracker
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (message) => {
|
||||
return message.id && message.content !== undefined && message.sender && message.type;
|
||||
}
|
||||
},
|
||||
isSubmittingForm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
formVisible: true
|
||||
};
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
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 &&
|
||||
((Array.isArray(this.message.formData.fields) && this.message.formData.fields.length > 0) ||
|
||||
(typeof this.message.formData.fields === 'object' && Object.keys(this.message.formData.fields).length > 0));
|
||||
},
|
||||
hasFormValues() {
|
||||
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
|
||||
// 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);
|
||||
|
||||
// Creëer een error message met correcte styling
|
||||
this.message.type = 'error';
|
||||
this.message.content = eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.';
|
||||
this.message.retryable = true;
|
||||
this.message.error = true; // Voeg error flag toe voor styling
|
||||
|
||||
// Bubble up naar parent component voor verdere afhandeling
|
||||
this.$emit('specialist-error', {
|
||||
messageId: this.message.id,
|
||||
...eventData
|
||||
});
|
||||
},
|
||||
|
||||
handleSpecialistComplete(eventData) {
|
||||
console.log('ChatMessage received specialist-complete event:', eventData);
|
||||
|
||||
// Update de inhoud van het bericht met het antwoord
|
||||
if (eventData.answer) {
|
||||
console.log('Updating message content with answer:', eventData.answer);
|
||||
this.message.content = eventData.answer;
|
||||
} else {
|
||||
console.error('No answer in specialist-complete event data');
|
||||
}
|
||||
|
||||
// Bubble up naar parent component voor eventuele verdere afhandeling
|
||||
this.$emit('specialist-complete', {
|
||||
messageId: this.message.id,
|
||||
answer: eventData.answer,
|
||||
form_request: eventData.form_request, // Wordt nu door ChatApp verwerkt
|
||||
result: eventData.result,
|
||||
interactionId: eventData.interactionId,
|
||||
taskId: eventData.taskId
|
||||
});
|
||||
},
|
||||
|
||||
formatMessage(content) {
|
||||
if (!content) return '';
|
||||
|
||||
// Enhanced markdown-like formatting
|
||||
return content
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||
.replace(/\n/g, '<br>');
|
||||
},
|
||||
|
||||
removeMessage() {
|
||||
// Dit zou een event moeten triggeren naar de parent component
|
||||
},
|
||||
|
||||
reactToMessage(emoji) {
|
||||
// Implementatie van reacties zou hier komen
|
||||
},
|
||||
|
||||
getMessageClass() {
|
||||
return `message ${this.message.sender}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* chat-message.css */
|
||||
|
||||
/* Algemene styling voor berichten */
|
||||
.message {
|
||||
max-width: 90%;
|
||||
margin-bottom: 15px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.ai {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
width: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Formulier styling */
|
||||
.form-display {
|
||||
margin: 15px 0;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(245, 245, 245, 0.7);
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Tabel styling voor formulieren */
|
||||
.form-result-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-result-table th {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-result-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-result-table td:first-child {
|
||||
font-weight: 500;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
/* Styling voor formulier invoervelden */
|
||||
.form-result-table input.form-input,
|
||||
.form-result-table textarea.form-textarea,
|
||||
.form-result-table select.form-select {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.form-result-table textarea.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Styling voor tabel cellen */
|
||||
.form-result-table .field-label {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 500;
|
||||
width: 35%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.form-result-table .field-value {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Toggle Switch styling */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-knob {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Material icon styling */
|
||||
.material-symbols-outlined {
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Zorgt dat het lettertype consistent is */
|
||||
.message-text {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Form error styling */
|
||||
.form-error {
|
||||
color: red;
|
||||
padding: 10px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
background-color: #ffebee;
|
||||
border: 1px solid #f44336;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 10px;
|
||||
padding: 8px 16px;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.message {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.form-result-table td:first-child {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
501
eveai_chat_client/static/assets/vue-components/DynamicForm.vue
Normal file
501
eveai_chat_client/static/assets/vue-components/DynamicForm.vue
Normal file
@@ -0,0 +1,501 @@
|
||||
<template>
|
||||
<div class="dynamic-form-container">
|
||||
<div class="dynamic-form">
|
||||
<!-- Form header with icon and title -->
|
||||
<div v-if="formData.title || formData.name || formData.icon" class="form-header">
|
||||
<div v-if="formData.icon" class="form-icon">
|
||||
<span class="material-symbols-outlined">{{ formData.icon }}</span>
|
||||
</div>
|
||||
<div class="form-title">{{ formData.title || formData.name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Form fields -->
|
||||
<div class="form-fields">
|
||||
<template v-if="Array.isArray(formData.fields)">
|
||||
<form-field
|
||||
v-for="field in formData.fields"
|
||||
:key="field.id || field.name"
|
||||
:field="field"
|
||||
:field-id="field.id || field.name"
|
||||
:model-value="localFormValues[field.id || field.name]"
|
||||
@update:model-value="updateFieldValue(field.id || field.name, $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="typeof formData.fields === 'object'">
|
||||
<form-field
|
||||
v-for="(field, fieldId) in formData.fields"
|
||||
:key="fieldId"
|
||||
:field="field"
|
||||
:field-id="fieldId"
|
||||
:model-value="localFormValues[fieldId]"
|
||||
@update:model-value="updateFieldValue(fieldId, $event)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Form actions (only show if not hidden and not read-only) -->
|
||||
<div v-if="!hideActions && !readOnly" class="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleCancel"
|
||||
class="btn btn-secondary"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Annuleren
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
class="btn btn-primary"
|
||||
:disabled="isSubmitting || !isFormValid"
|
||||
>
|
||||
<span v-if="isSubmitting">Verzenden...</span>
|
||||
<span v-else>Versturen</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Read-only form display -->
|
||||
<div v-if="readOnly" class="form-readonly">
|
||||
<div
|
||||
v-for="(field, fieldId) in getFieldsForDisplay()"
|
||||
:key="fieldId"
|
||||
class="form-field-readonly"
|
||||
>
|
||||
<div class="field-label">{{ field.name }}:</div>
|
||||
<div class="field-value" :class="{'text-value': field.type === 'text'}">
|
||||
{{ formatFieldValue(fieldId, field) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormField from './FormField.vue';
|
||||
|
||||
export default {
|
||||
name: 'DynamicForm',
|
||||
components: {
|
||||
'form-field': FormField
|
||||
},
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (formData) => {
|
||||
// Controleer eerst of formData een geldig object is
|
||||
if (!formData || typeof formData !== 'object') {
|
||||
console.error('FormData is niet een geldig object');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of er een titel of naam is
|
||||
if (!formData.title && !formData.name) {
|
||||
console.error('FormData heeft geen title of name');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of er velden zijn
|
||||
if (!formData.fields) {
|
||||
console.error('FormData heeft geen fields eigenschap');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of velden een array of object zijn
|
||||
if (!Array.isArray(formData.fields) && typeof formData.fields !== 'object') {
|
||||
console.error('FormData.fields is geen array of object');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('FormData is geldig:', formData);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
formValues: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['submit', 'cancel', 'update:formValues'],
|
||||
data() {
|
||||
return {
|
||||
localFormValues: { ...this.formValues }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
// Basic validation - check required fields
|
||||
const missingFields = [];
|
||||
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
// Valideer array-gebaseerde velden
|
||||
this.formData.fields.forEach(field => {
|
||||
const fieldId = field.id || field.name;
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Valideer object-gebaseerde velden
|
||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields.length === 0;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
formValues: {
|
||||
handler(newValues) {
|
||||
// Gebruik een vlag om recursieve updates te voorkomen
|
||||
if (JSON.stringify(newValues) !== JSON.stringify(this.localFormValues)) {
|
||||
this.localFormValues = JSON.parse(JSON.stringify(newValues));
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
localFormValues: {
|
||||
handler(newValues) {
|
||||
// Gebruik een vlag om recursieve updates te voorkomen
|
||||
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
|
||||
this.$emit('update:formValues', JSON.parse(JSON.stringify(newValues)));
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateFieldValue(fieldId, value) {
|
||||
// Update lokale waarde
|
||||
this.localFormValues = {
|
||||
...this.localFormValues,
|
||||
[fieldId]: value
|
||||
};
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
// Basic validation
|
||||
const missingFields = [];
|
||||
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
// Valideer array-gebaseerde velden
|
||||
this.formData.fields.forEach(field => {
|
||||
const fieldId = field.id || field.name;
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Valideer object-gebaseerde velden
|
||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
alert(`De volgende velden zijn verplicht: ${missingFields.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit submit event
|
||||
this.$emit('submit', this.localFormValues);
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
getFieldsForDisplay() {
|
||||
// Voor read-only weergave
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
const fieldsObj = {};
|
||||
this.formData.fields.forEach(field => {
|
||||
const fieldId = field.id || field.name;
|
||||
fieldsObj[fieldId] = field;
|
||||
});
|
||||
return fieldsObj;
|
||||
}
|
||||
return this.formData.fields;
|
||||
},
|
||||
|
||||
formatFieldValue(fieldId, field) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
// Format different field types
|
||||
if (field.type === 'boolean') {
|
||||
return value ? 'Ja' : 'Nee';
|
||||
} else if (field.type === 'enum' && !value && field.default) {
|
||||
return field.default;
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Dynamisch formulier stijlen */
|
||||
.dynamic-form-container {
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.dynamic-form {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.form-fields {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-field input,
|
||||
.form-field select,
|
||||
.form-field textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field select:focus,
|
||||
.form-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.form-field textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.field-description {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 0.8rem;
|
||||
color: #777;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
color: #555;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-toggle-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.form-toggle-btn.active {
|
||||
color: #4a90e2;
|
||||
background-color: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #e53935;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Read-only form styling */
|
||||
.form-readonly {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.form-field-readonly {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
flex: 0 0 30%;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
328
eveai_chat_client/static/assets/vue-components/FormField.vue
Normal file
328
eveai_chat_client/static/assets/vue-components/FormField.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="form-field" style="margin-bottom: 15px; display: grid; grid-template-columns: 35% 65%; align-items: center;">
|
||||
<!-- Label voor alle velden behalve boolean/checkbox die een speciale behandeling krijgen -->
|
||||
<label v-if="fieldType !== 'checkbox'" :for="fieldId" style="margin-right: 10px; font-weight: 500;">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Container voor input velden -->
|
||||
<div style="width: 100%;">
|
||||
<!-- Context informatie indien aanwezig -->
|
||||
<div v-if="field.context" class="field-context" style="margin-bottom: 8px; font-size: 0.9em; color: #666; background-color: #f8f9fa; padding: 8px; border-radius: 4px; border-left: 3px solid #4285f4;">
|
||||
{{ field.context }}
|
||||
</div>
|
||||
|
||||
<!-- Tekstinvoer (string/str) -->
|
||||
<input
|
||||
v-if="fieldType === 'text'"
|
||||
:id="fieldId"
|
||||
type="text"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Numerieke invoer (int/float) -->
|
||||
<input
|
||||
v-if="fieldType === 'number'"
|
||||
:id="fieldId"
|
||||
type="number"
|
||||
v-model.number="value"
|
||||
:required="field.required"
|
||||
:step="stepValue"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Tekstvlak (text) -->
|
||||
<textarea
|
||||
v-if="fieldType === 'textarea'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:rows="field.rows || 3"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box; resize: vertical;"
|
||||
></textarea>
|
||||
|
||||
<!-- Selectielijst (enum) -->
|
||||
<select
|
||||
v-if="fieldType === 'select'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
<option value="">Selecteer een optie</option>
|
||||
<option
|
||||
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Radio buttons (options) -->
|
||||
<div v-if="fieldType === 'radio'" class="radio-group" style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<label
|
||||
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
class="radio-label"
|
||||
style="display: flex; align-items: center; cursor: pointer;"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:name="fieldId"
|
||||
:value="option"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<span>{{ option }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox (boolean) -->
|
||||
<div v-if="fieldType === 'checkbox'" class="checkbox-container" style="display: flex; align-items: center;">
|
||||
<label class="checkbox-label" style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<span class="checkbox-text">{{ field.name }}</span>
|
||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Bestandsupload -->
|
||||
<input
|
||||
v-if="fieldType === 'file'"
|
||||
:id="fieldId"
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
:required="field.required"
|
||||
:accept="field.accept || '*'"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Datum invoer -->
|
||||
<input
|
||||
v-if="fieldType === 'date'"
|
||||
:id="fieldId"
|
||||
type="date"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Tijd invoer -->
|
||||
<input
|
||||
v-if="fieldType === 'time'"
|
||||
:id="fieldId"
|
||||
type="time"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Email invoer -->
|
||||
<input
|
||||
v-if="fieldType === 'email'"
|
||||
:id="fieldId"
|
||||
type="email"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- URL invoer -->
|
||||
<input
|
||||
v-if="fieldType === 'url'"
|
||||
:id="fieldId"
|
||||
type="url"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Beschrijving/help tekst -->
|
||||
<small v-if="description" class="field-description" style="display: block; margin-top: 5px; font-size: 0.8rem; color: #777; line-height: 1.4;">
|
||||
{{ description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormField',
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (field) => {
|
||||
return field.name && field.type;
|
||||
}
|
||||
},
|
||||
fieldId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
value: {
|
||||
get() {
|
||||
// Gebruik default waarde als modelValue undefined is
|
||||
if (this.modelValue === undefined || this.modelValue === null) {
|
||||
if (this.field.type === 'boolean') {
|
||||
return this.field.default === true;
|
||||
}
|
||||
return this.field.default !== undefined ? this.field.default : '';
|
||||
}
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
// Voorkom emit als de waarde niet is veranderd
|
||||
if (JSON.stringify(value) !== JSON.stringify(this.modelValue)) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
fieldType() {
|
||||
// Map Python types naar HTML input types
|
||||
const typeMap = {
|
||||
'str': 'text',
|
||||
'string': 'text',
|
||||
'int': 'number',
|
||||
'integer': 'number',
|
||||
'float': 'number',
|
||||
'text': 'textarea',
|
||||
'enum': 'select',
|
||||
'options': 'radio',
|
||||
'boolean': 'checkbox',
|
||||
'file': 'file',
|
||||
'date': 'date',
|
||||
'time': 'time',
|
||||
'email': 'email',
|
||||
'url': 'url'
|
||||
};
|
||||
return typeMap[this.field.type] || this.field.type;
|
||||
},
|
||||
stepValue() {
|
||||
return this.field.type === 'float' ? 'any' : 1;
|
||||
},
|
||||
description() {
|
||||
return this.field.description || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.value = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field select:focus,
|
||||
.form-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2 !important;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.field-description {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 0.8rem;
|
||||
color: #777;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.field-context {
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
background-color: #f8f9fa;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4285f4;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #d93025;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.form-field {
|
||||
display: block !important;
|
||||
grid-template-columns: none !important;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
286
eveai_chat_client/static/assets/vue-components/FormMessage.vue
Normal file
286
eveai_chat_client/static/assets/vue-components/FormMessage.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div v-if="hasFormData" class="form-message">
|
||||
<div v-if="formData.name" class="form-message-header">
|
||||
<i v-if="formData.icon" class="material-icons form-message-icon">{{ formData.icon }}</i>
|
||||
<span class="form-message-title">{{ formData.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-message-fields">
|
||||
<div v-for="field in formattedFields" :key="field.id" class="form-message-field">
|
||||
<div class="field-message-label">{{ field.name }}:</div>
|
||||
<div class="field-message-value" :class="{'text-value': field.type === 'text'}">{{ field.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormMessage',
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
formValues: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasFormData() {
|
||||
return this.formData && this.formData.fields && Object.keys(this.formData.fields).length > 0;
|
||||
},
|
||||
formattedFields() {
|
||||
if (!this.hasFormData) return [];
|
||||
|
||||
return Object.entries(this.formData.fields).map(([fieldId, field]) => {
|
||||
let displayValue = this.formValues[fieldId] || '';
|
||||
|
||||
// Format different field types
|
||||
if (field.type === 'boolean') {
|
||||
displayValue = displayValue ? 'Ja' : 'Nee';
|
||||
} else if (field.type === 'enum' && !displayValue && field.default) {
|
||||
displayValue = field.default;
|
||||
} else if (field.type === 'text') {
|
||||
// Voor tekstgebieden, behoud witruimte
|
||||
// De CSS zal dit tonen met white-space: pre-wrap
|
||||
}
|
||||
|
||||
return {
|
||||
id: fieldId,
|
||||
name: field.name,
|
||||
value: displayValue || '-',
|
||||
type: field.type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styling voor formulier in berichten */
|
||||
.form-message {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(245, 245, 245, 0.7);
|
||||
padding: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.form-message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.form-message-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 18px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-message-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.form-message-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-message-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-message-field:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.field-message-label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
flex: 0 0 40%;
|
||||
padding-right: 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.field-message-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.field-message-value.text-value {
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Styling voor verschillende message contexten */
|
||||
.message.user .form-message {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.message.ai .form-message {
|
||||
background-color: rgba(245, 245, 250, 0.7);
|
||||
}
|
||||
|
||||
/* Algemene form display styling */
|
||||
.form-display {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-form-values {
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Speciale styling voor read-only formulieren in user messages */
|
||||
.user-form .form-field {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.user-form .field-label {
|
||||
font-weight: 500 !important;
|
||||
color: #555 !important;
|
||||
padding: 2px 0 !important;
|
||||
}
|
||||
|
||||
.user-form .field-value {
|
||||
padding: 2px 0 !important;
|
||||
}
|
||||
|
||||
/* Schakel hover effecten uit voor read-only formulieren */
|
||||
.read-only .form-field:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Subtiele scheiding tussen velden */
|
||||
.dynamic-form.read-only .form-fields {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* Verklein vorm titels in berichten */
|
||||
.message-form .form-title {
|
||||
font-size: 1em !important;
|
||||
}
|
||||
|
||||
.message-form .form-description {
|
||||
font-size: 0.85em !important;
|
||||
}
|
||||
|
||||
.form-readonly {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-readonly .field-label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-readonly .field-value {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.form-readonly .text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Algemene styling verbetering voor berichten */
|
||||
.message-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.form-message-field {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.field-message-label {
|
||||
flex: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.field-message-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-message {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form-message-title {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.form-message {
|
||||
background-color: rgba(40, 40, 40, 0.7);
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.form-message-header {
|
||||
border-bottom-color: #555;
|
||||
}
|
||||
|
||||
.form-message-title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.field-message-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.field-message-value {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.form-message-field {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.form-message {
|
||||
border: 2px solid #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-message-title,
|
||||
.field-message-label,
|
||||
.field-message-value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.form-message-header {
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<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)"
|
||||
class="language-select"
|
||||
>
|
||||
<option
|
||||
v-for="lang in getAvailableLanguages()"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
>
|
||||
{{ lang.flag }} {{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LanguageSelector',
|
||||
props: {
|
||||
initialLanguage: {
|
||||
type: String,
|
||||
default: 'nl'
|
||||
},
|
||||
currentLanguage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
supportedLanguageDetails: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowedLanguages: {
|
||||
type: Array,
|
||||
default: () => ['nl', 'en', 'fr', 'de']
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const startLanguage = this.currentLanguage || this.initialLanguage;
|
||||
return {
|
||||
selectedLanguage: startLanguage,
|
||||
internalCurrentLanguage: startLanguage
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log('🔍 [DEBUG] LanguageSelector mounted with Vue SFC');
|
||||
console.log('🔍 [DEBUG] Props:', {
|
||||
initialLanguage: this.initialLanguage,
|
||||
currentLanguage: this.currentLanguage,
|
||||
supportedLanguageDetails: this.supportedLanguageDetails,
|
||||
allowedLanguages: this.allowedLanguages
|
||||
});
|
||||
|
||||
// Emit initial language
|
||||
this.$emit('language-changed', this.selectedLanguage);
|
||||
|
||||
// DOM event
|
||||
const event = new CustomEvent('vue:language-changed', {
|
||||
detail: { language: this.selectedLanguage }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
methods: {
|
||||
getAvailableLanguages() {
|
||||
const languages = [];
|
||||
const languagesToUse = (this.allowedLanguages && this.allowedLanguages.length > 0)
|
||||
? this.allowedLanguages
|
||||
: ['nl', 'en', 'fr', 'de'];
|
||||
|
||||
if (this.supportedLanguageDetails && Object.keys(this.supportedLanguageDetails).length > 0) {
|
||||
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 {
|
||||
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].name,
|
||||
flag: defaultLanguages[code].flag
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return languages;
|
||||
},
|
||||
|
||||
changeLanguage(languageCode) {
|
||||
console.log(`LanguageSelector: changeLanguage called with ${languageCode}`);
|
||||
|
||||
if (this.internalCurrentLanguage !== languageCode) {
|
||||
this.internalCurrentLanguage = languageCode;
|
||||
this.selectedLanguage = languageCode;
|
||||
|
||||
// Vue component emit
|
||||
this.$emit('language-changed', languageCode);
|
||||
|
||||
// DOM event
|
||||
const event = new CustomEvent('vue:language-changed', {
|
||||
detail: { language: languageCode }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
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-select:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.language-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.3);
|
||||
}
|
||||
|
||||
.language-select option {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="message-history-container">
|
||||
<!-- Messages container -->
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<!-- Loading indicator for load more -->
|
||||
<div v-if="$slots.loading" class="load-more-indicator">
|
||||
<slot name="loading"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="messages.length === 0" class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<div class="empty-text">Nog geen berichten</div>
|
||||
<div class="empty-subtext">Start een gesprek door een bericht te typen!</div>
|
||||
</div>
|
||||
|
||||
<!-- Message list -->
|
||||
<template v-else>
|
||||
<!-- Messages -->
|
||||
<template v-for="(message, index) in messages" :key="message.id">
|
||||
<!-- The actual message -->
|
||||
<chat-message
|
||||
:message="message"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
></chat-message>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<typing-indicator v-if="isTyping"></typing-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatMessage from './ChatMessage.vue';
|
||||
import TypingIndicator from './TypingIndicator.vue';
|
||||
|
||||
export default {
|
||||
name: 'MessageHistory',
|
||||
components: {
|
||||
'chat-message': ChatMessage,
|
||||
'typing-indicator': TypingIndicator
|
||||
},
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isTyping: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isSubmittingForm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['load-more', 'specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
isAtBottom: true,
|
||||
showScrollButton: false,
|
||||
unreadCount: 0,
|
||||
languageChangeHandler: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
messages: {
|
||||
handler(newMessages, oldMessages) {
|
||||
// Auto-scroll when new messages are added
|
||||
if (this.autoScroll && newMessages.length > (oldMessages?.length || 0)) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
isTyping(newVal) {
|
||||
if (newVal && this.autoScroll) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 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);
|
||||
},
|
||||
mounted() {
|
||||
this.setupScrollListener();
|
||||
|
||||
// Initial scroll to bottom
|
||||
if (this.autoScroll) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Cleanup scroll listener
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
// Cleanup language change listener
|
||||
if (this.languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLanguageChange(event) {
|
||||
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
||||
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
|
||||
const firstMessage = this.messages[0];
|
||||
|
||||
// Controleer of we een originele inhoud hebben om te vertalen
|
||||
if (firstMessage.originalContent) {
|
||||
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(
|
||||
firstMessage.originalContent,
|
||||
event.detail.language,
|
||||
null, // source_lang (auto-detect)
|
||||
'chat_message', // context
|
||||
this.apiPrefix // API prefix voor tenant routing
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fout bij vertalen eerste bericht:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.isAtBottom = true;
|
||||
this.showScrollButton = false;
|
||||
this.unreadCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
setupScrollListener() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (!container) return;
|
||||
|
||||
const threshold = 100; // pixels from bottom
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||
|
||||
this.isAtBottom = isNearBottom;
|
||||
|
||||
// Load more messages when scrolled to top
|
||||
if (container.scrollTop === 0) {
|
||||
this.$emit('load-more');
|
||||
}
|
||||
},
|
||||
|
||||
handleImageLoaded() {
|
||||
// Auto-scroll when images load to maintain position
|
||||
if (this.isAtBottom) {
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
}
|
||||
},
|
||||
|
||||
searchMessages(query) {
|
||||
// Simple message search
|
||||
if (!query.trim()) return this.messages;
|
||||
|
||||
const searchTerm = query.toLowerCase();
|
||||
return this.messages.filter(message =>
|
||||
message.content &&
|
||||
message.content.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-history-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.load-more-indicator {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
max-width: 300px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.chat-messages {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<div class="progress-tracker" :class="{ 'expanded': isExpanded, 'completed': isCompleted && !hasError, 'error': error || hasError }">
|
||||
<div
|
||||
class="progress-header"
|
||||
@click="toggleExpand"
|
||||
:title="isExpanded ? 'Inklappen' : 'Uitklappen voor volledige voortgang'"
|
||||
>
|
||||
<div class="progress-title">
|
||||
<span v-if="connecting" class="spinner"></span>
|
||||
<span v-else-if="error" class="status-icon error">✗</span>
|
||||
<span v-else-if="isCompleted" class="status-icon completed">✓</span>
|
||||
<span v-else class="status-icon in-progress"></span>
|
||||
<span v-if="error">Fout bij verwerking</span>
|
||||
<span v-else-if="isCompleted">Verwerking voltooid</span>
|
||||
<span v-else>Bezig met redeneren...</span>
|
||||
</div>
|
||||
<div class="progress-toggle">
|
||||
{{ isExpanded ? '▲' : '▼' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="progress-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="progressContainer"
|
||||
class="progress-content"
|
||||
:class="{ 'single-line': !isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in displayLines"
|
||||
:key="index"
|
||||
class="progress-line"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProgressTracker',
|
||||
props: {
|
||||
taskId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
isCompleted: false,
|
||||
hasError: false,
|
||||
connecting: true,
|
||||
error: null,
|
||||
progressLines: [],
|
||||
eventSource: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayLines() {
|
||||
if (this.isExpanded) {
|
||||
return this.progressLines;
|
||||
} else {
|
||||
// Show only the last line when collapsed
|
||||
return this.progressLines.length > 0 ? [this.progressLines[this.progressLines.length - 1]] : [];
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connectToProgressStream();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.disconnectEventSource();
|
||||
},
|
||||
methods: {
|
||||
connectToProgressStream() {
|
||||
if (!this.taskId) {
|
||||
console.error('Geen task ID beschikbaar voor progress tracking');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting to progress stream for task:', this.taskId);
|
||||
|
||||
// Construct the SSE URL
|
||||
const baseUrl = window.location.origin;
|
||||
const sseUrl = `${baseUrl}${this.apiPrefix}/api/progress/${this.taskId}`;
|
||||
|
||||
console.log('SSE URL:', sseUrl);
|
||||
|
||||
try {
|
||||
this.eventSource = new EventSource(sseUrl);
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
console.log('Progress stream connected');
|
||||
this.connecting = false;
|
||||
};
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
this.handleProgressUpdate(event);
|
||||
};
|
||||
|
||||
this.eventSource.onerror = (event) => {
|
||||
console.error('Progress stream error:', event);
|
||||
this.handleError(event);
|
||||
};
|
||||
|
||||
// Listen for specific event types
|
||||
this.eventSource.addEventListener('progress', (event) => {
|
||||
this.handleProgressUpdate(event);
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('complete', (event) => {
|
||||
this.handleSpecialistComplete(event);
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('error', (event) => {
|
||||
this.handleSpecialistError(event);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create EventSource:', error);
|
||||
this.error = 'Kan geen verbinding maken met de voortgangsstream.';
|
||||
this.connecting = false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnectEventSource() {
|
||||
if (this.eventSource) {
|
||||
console.log('Disconnecting progress stream');
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleProgressUpdate(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Progress update:', data);
|
||||
|
||||
if (data.message) {
|
||||
this.progressLines.push(data.message);
|
||||
|
||||
// Auto-scroll to bottom if expanded
|
||||
if (this.isExpanded) {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.progressContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing progress data:', error);
|
||||
}
|
||||
},
|
||||
|
||||
handleSpecialistComplete(event) {
|
||||
console.log('Specialist complete event:', event);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Specialist complete data:', data);
|
||||
|
||||
this.isCompleted = true;
|
||||
this.connecting = false;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Emit the complete event to parent
|
||||
if (data.result && data.result.answer) {
|
||||
this.$emit('specialist-complete', {
|
||||
answer: data.result.answer,
|
||||
form_request: data.result.form_request,
|
||||
result: data.result,
|
||||
interactionId: data.interaction_id,
|
||||
taskId: this.taskId
|
||||
});
|
||||
} else {
|
||||
console.error('Missing result.answer in specialist complete data:', data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing specialist complete data:', error);
|
||||
this.handleSpecialistError({ data: JSON.stringify({ Error: 'Failed to parse completion data' }) });
|
||||
}
|
||||
},
|
||||
|
||||
handleSpecialistError(event) {
|
||||
console.log('Specialist error event:', event);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Specialist error data:', data);
|
||||
|
||||
this.isCompleted = true;
|
||||
this.hasError = true;
|
||||
this.connecting = false;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Set user-friendly error message
|
||||
const errorMessage = "We could not process your request. Please try again later.";
|
||||
this.error = errorMessage;
|
||||
|
||||
// Log the actual error for debug purposes
|
||||
if (data.Error) {
|
||||
console.error('Specialist Error:', data.Error);
|
||||
}
|
||||
|
||||
// Emit error event to parent
|
||||
this.$emit('specialist-error', {
|
||||
message: errorMessage,
|
||||
originalError: data.Error,
|
||||
taskId: this.taskId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing specialist error data:', error);
|
||||
this.error = 'Er is een onbekende fout opgetreden.';
|
||||
this.isCompleted = true;
|
||||
this.hasError = true;
|
||||
this.connecting = false;
|
||||
this.disconnectEventSource();
|
||||
}
|
||||
},
|
||||
|
||||
handleError(event) {
|
||||
console.error('SSE Error event:', event);
|
||||
this.error = 'Er is een fout opgetreden bij het verwerken van updates.';
|
||||
this.connecting = false;
|
||||
|
||||
// Try to parse error data
|
||||
try {
|
||||
const errorData = JSON.parse(event.data);
|
||||
if (errorData && errorData.message) {
|
||||
this.error = errorData.message;
|
||||
}
|
||||
} catch (err) {
|
||||
// Keep generic error message if parsing fails
|
||||
}
|
||||
},
|
||||
|
||||
toggleExpand() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
|
||||
if (this.isExpanded) {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.progressContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-tracker {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
background-color: #f9f9f9;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-tracker.expanded {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-tracker.completed {
|
||||
border-color: #4caf50;
|
||||
background-color: #f1f8e9;
|
||||
}
|
||||
|
||||
.progress-tracker.error {
|
||||
border-color: #f44336;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-header:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-icon.completed {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.status-icon.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.status-icon.in-progress {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #2196f3;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-toggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-tracker.expanded .progress-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.progress-error {
|
||||
padding: 10px 15px;
|
||||
background-color: #ffcdd2;
|
||||
color: #c62828;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 15px;
|
||||
background-color: #fff;
|
||||
border-radius: 0 0 8px 8px;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-content.single-line {
|
||||
max-height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
padding: 2px 0;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.progress-line:last-child {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.progress-content::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.progress-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.progress-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.progress-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.progress-header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
padding: 8px 12px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.progress-content.single-line {
|
||||
max-height: 35px;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for new progress lines */
|
||||
.progress-line {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="typing-indicator">
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
<div v-if="showText" class="typing-text">{{ text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TypingIndicator',
|
||||
props: {
|
||||
showText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: 'Bezig met typen...'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Andere methoden kunnen hier staan
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
margin: 10px 0;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 18px;
|
||||
max-width: 80px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
margin: 0 2px;
|
||||
animation: typing-bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
margin-left: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes typing-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alternative pulse animation */
|
||||
@keyframes typing-pulse {
|
||||
0%, 60%, 100% {
|
||||
transform: initial;
|
||||
opacity: 0.4;
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.typing-indicator {
|
||||
padding: 8px 12px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
font-size: 0.8rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.typing-indicator {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.typing-indicator {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.typing-dot {
|
||||
animation: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
eveai_chat_client/static/assets/vue-components/index.js
Normal file
15
eveai_chat_client/static/assets/vue-components/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Vue Components Barrel Export
|
||||
// Export all Vue Single File Components
|
||||
|
||||
export { default as LanguageSelector } from './LanguageSelector.vue';
|
||||
export { default as ChatInput } from './ChatInput.vue';
|
||||
export { default as MessageHistory } from './MessageHistory.vue';
|
||||
export { default as ChatMessage } from './ChatMessage.vue';
|
||||
export { default as TypingIndicator } from './TypingIndicator.vue';
|
||||
export { default as ProgressTracker } from './ProgressTracker.vue';
|
||||
export { default as DynamicForm } from './DynamicForm.vue';
|
||||
export { default as FormField } from './FormField.vue';
|
||||
export { default as FormMessage } from './FormMessage.vue';
|
||||
|
||||
// Log successful loading
|
||||
console.log('Vue components loaded successfully from barrel export');
|
||||
Reference in New Issue
Block a user