Eerste goed werkende versie van een formulier in de chat input.

This commit is contained in:
Josako
2025-06-13 17:27:49 +02:00
parent f1c60f9574
commit 2835486599
9 changed files with 547 additions and 291 deletions

View File

@@ -1,9 +1,21 @@
// static/js/components/ChatInput.js
export const ChatInput = {
// Importeer de IconManager (als module systeem wordt gebruikt)
// Anders moet je ervoor zorgen dat MaterialIconManager.js eerder wordt geladen
// en iconManager beschikbaar is via window.iconManager
export const ChatInput = {
name: 'ChatInput',
components: {
'dynamic-form': window.__vueApp ? DynamicForm : null
'dynamic-form': window.DynamicForm
},
created() {
// Als module systeem wordt gebruikt:
// import { iconManager } from './MaterialIconManager.js';
// Anders gebruiken we window.iconManager als het beschikbaar is:
if (window.iconManager && this.formData && this.formData.icon) {
window.iconManager.ensureIconsLoaded({}, [this.formData.icon]);
}
},
props: {
currentMessage: {
@@ -29,18 +41,44 @@ export const ChatInput = {
},
emits: ['send-message', 'update-message', 'submit-form'],
watch: {
formData: {
handler(newFormData) {
console.log('ChatInput received formData:', newFormData);
if (newFormData) {
this.formValues = {}; // Reset formulierwaarden
this.showForm = true;
} else {
this.showForm = false;
'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;
},
@@ -52,8 +90,7 @@ export const ChatInput = {
data() {
return {
localMessage: this.currentMessage,
formValues: {},
showForm: false
formValues: {}
};
},
computed: {
@@ -72,20 +109,53 @@ export const ChatInput = {
},
canSend() {
const hasValidForm = this.showForm && this.formData && this.validateForm();
const hasValidForm = this.formData && this.validateForm();
const hasValidMessage = this.localMessage.trim() && !this.isOverLimit;
return (!this.isLoading) && (hasValidForm || hasValidMessage);
},
sendButtonText() {
return this.showForm ? 'Verstuur formulier' : 'Verstuur bericht';
if (this.isLoading) {
return 'Verzenden...';
}
return this.formData ? 'Verstuur formulier' : 'Verstuur bericht';
}
},
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));
}
},
methods: {
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();
@@ -98,14 +168,12 @@ export const ChatInput = {
sendMessage() {
if (!this.canSend) return;
if (this.showForm && this.formData) {
if (this.formData) {
// Valideer het formulier
if (this.validateForm()) {
// Verstuur het formulier
this.$emit('submit-form', this.formValues);
this.formValues = {};
// Reset het formulier na verzenden
this.showForm = false;
}
} else if (this.localMessage.trim()) {
// Verstuur normaal bericht
@@ -113,6 +181,12 @@ export const ChatInput = {
}
},
// Deze methode houden we voor de volledigheid, maar zal niet meer direct worden aangeroepen
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;
@@ -160,42 +234,39 @@ export const ChatInput = {
this.focusInput();
},
toggleForm() {
this.showForm = !this.showForm;
if (!this.showForm) {
this.focusInput();
}
},
submitForm() {
if (this.canSubmitForm) {
this.$emit('submit-form', { ...this.formValues });
this.showForm = false;
this.focusInput();
}
},
cancelForm() {
this.showForm = false;
this.focusInput();
},
updateFormValues(newValues) {
this.formValues = { ...newValues };
}
},
template: `
<div class="chat-input-container">
<div v-if="formData && showForm" class="dynamic-form-container">
<!-- Dynamisch toevoegen van Material Symbols Outlined voor iconen -->
<div v-if="formData && formData.icon" class="material-icons-container">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
</div>
<!-- Dynamisch formulier container -->
<div v-if="formData" class="dynamic-form-container" style="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);">
<!-- 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>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="chat-input">
<!-- Main input area -->
<div class="input-main">
@@ -218,26 +289,15 @@ export const ChatInput = {
</div>
<!-- Input actions -->
<div class="input-actions">
<!-- Formulier toggle knop -->
<button
v-if="hasFormData"
@click="toggleForm"
class="form-toggle-btn"
:disabled="isLoading"
:class="{ 'active': showForm }"
:title="showForm ? 'Verberg formulier' : 'Toon formulier'"
>
<i class="material-icons">description</i>
</button>
<div class="input-actions">
<!-- Send button -->
<!-- Universele verzendknop (voor zowel berichten als formulieren) -->
<button
@click="sendMessage"
class="send-btn"
:class="{ 'form-mode': showForm && formData }"
:class="{ 'form-mode': formData }"
:disabled="!canSend"
:title="showForm ? 'Verstuur formulier' : 'Verstuur bericht'"
: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">

View File

@@ -8,10 +8,6 @@ export const ChatMessage = {
return message.id && message.content !== undefined && message.sender && message.type;
}
},
formValues: {
type: Object,
default: () => ({})
},
isSubmittingForm: {
type: Boolean,
default: false
@@ -21,7 +17,7 @@ export const ChatMessage = {
default: ''
}
},
emits: ['submit-form', 'image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
data() {
return {
};
@@ -57,7 +53,11 @@ export const ChatMessage = {
// Bubble up naar parent component voor eventuele verdere afhandeling
this.$emit('specialist-complete', {
messageId: this.message.id,
...eventData
answer: eventData.answer,
form_request: eventData.form_request, // Wordt nu door ChatApp verwerkt
result: eventData.result,
interactionId: eventData.interactionId,
taskId: eventData.taskId
});
},
@@ -73,9 +73,6 @@ export const ChatMessage = {
.replace(/\n/g, '<br>');
},
submitForm() {
this.$emit('submit-form', this.message.formData, this.message.id);
},
removeMessage() {
// Dit zou een event moeten triggeren naar de parent component
@@ -86,9 +83,6 @@ export const ChatMessage = {
},
getMessageClass() {
if (this.message.type === 'form') {
return 'form-message';
}
return `message ${this.message.sender}`;
}
},
@@ -157,16 +151,6 @@ export const ChatMessage = {
</div>
</template>
<!-- Dynamic forms -->
<template v-if="message.type === 'form'">
<dynamic-form
:form-data="message.formData"
:form-values="formValues[message.id] || {}"
:is-submitting="isSubmittingForm"
@submit="submitForm"
@cancel="removeMessage"
></dynamic-form>
</template>
<!-- System messages -->
<template v-if="message.type === 'system'">

View File

@@ -1,12 +1,42 @@
export const DynamicForm = {
name: 'DynamicForm',
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);
}
},
props: {
formData: {
type: Object,
required: true,
validator: (formData) => {
return formData && formData.title && formData.fields &&
(Array.isArray(formData.fields) || typeof formData.fields === 'object');
// 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: {
@@ -44,6 +74,14 @@ export const DynamicForm = {
this.$emit('update:formValues', newValues);
},
deep: true
},
'formData.icon': {
handler(newIcon) {
if (newIcon && window.iconManager) {
window.iconManager.loadIcon(newIcon);
}
},
immediate: true
}
},
methods: {
@@ -97,11 +135,14 @@ export const DynamicForm = {
},
template: `
<div class="dynamic-form" :class="{ 'read-only': readOnly }">
<div class="form-header" v-if="formData.title || formData.icon">
<div class="form-icon" v-if="formData.icon">
<i class="material-icons">{{ formData.icon }}</i>
<div class="form-header" v-if="formData.title || formData.name || formData.icon" style="margin-bottom: 20px; display: flex; align-items: center;">
<div class="form-icon" v-if="formData.icon" style="margin-right: 10px; display: flex; align-items: center;">
<span class="material-symbols-outlined" style="font-size: 24px; color: #4285f4;">{{ formData.icon }}</span>
</div>
<div>
<div class="form-title" style="font-weight: bold; font-size: 1.2em; color: #333;">{{ formData.title || formData.name }}</div>
<div v-if="formData.description" class="form-description" style="margin-top: 5px; color: #666; font-size: 0.9em;">{{ formData.description }}</div>
</div>
<div class="form-title">{{ formData.title }}</div>
</div>
<div v-if="readOnly" class="form-readonly">
@@ -148,7 +189,7 @@ export const DynamicForm = {
</div>
<form v-else @submit.prevent="handleSubmit" novalidate>
<div class="form-fields">
<div class="form-fields" style="margin-top: 10px;">
<template v-if="Array.isArray(formData.fields)">
<form-field
v-for="field in formData.fields"

View File

@@ -63,84 +63,109 @@ export const FormField = {
}
},
template: `
<div class="form-field">
<label :for="fieldId">
{{ field.name }}
<span v-if="field.required" class="required">*</span>
<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>
<!-- Tekstinvoer (string/str) -->
<input
v-if="fieldType === 'text'"
:id="fieldId"
type="text"
v-model="value"
:required="field.required"
:placeholder="field.placeholder || ''"
:title="description"
>
<!-- 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"
>
<!-- Tekstvlak (text) -->
<textarea
v-if="fieldType === 'textarea'"
:id="fieldId"
v-model="value"
:required="field.required"
:rows="field.rows || 3"
:placeholder="field.placeholder || ''"
:title="description"
></textarea>
<!-- Dropdown (enum) -->
<select
v-if="fieldType === 'select'"
:id="fieldId"
v-model="value"
:required="field.required"
:title="description"
>
<option v-if="!field.required" value="">Selecteer een optie</option>
<option
v-for="option in (field.allowedValues || field.allowed_values || [])"
:key="option"
:value="option"
<!-- Container voor input velden -->
<div style="width: 100%;">
<!-- 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;"
>
{{ option }}
</option>
</select>
<!-- Debug info voor select field -->
<div v-if="fieldType === 'select' && (!(field.allowedValues || field.allowed_values) || (field.allowedValues || field.allowed_values).length === 0)" class="field-error">
Geen opties beschikbaar voor dit veld.
<pre style="font-size: 10px; color: #999;">{{ JSON.stringify(field, null, 2) }}</pre>
<!-- 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; resize: vertical; box-sizing: border-box;"
></textarea>
<!-- Dropdown (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; background-color: white; box-sizing: border-box;"
>
<option v-if="!field.required" value="">Selecteer een optie</option>
<option
v-for="option in (field.allowedValues || field.allowed_values || [])"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<!-- Debug info voor select field -->
<div v-if="fieldType === 'select' && (!(field.allowedValues || field.allowed_values) || (field.allowedValues || field.allowed_values).length === 0)"
style="color: #d93025; font-size: 0.85em; margin-top: 4px;">
Geen opties beschikbaar voor dit veld.
</div>
</div>
<!-- Checkbox (boolean) -->
<div v-if="fieldType === 'checkbox'" class="checkbox-container">
<label class="checkbox-label">
<!-- Toggle switch voor boolean velden, met speciale layout voor deze velden -->
<div v-if="fieldType === 'checkbox'" style="grid-column: 1 / span 2; display: flex; align-items: center;">
<div class="toggle-switch" style="position: relative; display: inline-block; width: 50px; height: 24px;">
<input
:id="fieldId"
type="checkbox"
v-model="value"
:required="field.required"
:title="description"
style="opacity: 0; width: 0; height: 0;"
>
<span class="checkbox-text">{{ field.description || 'Ja' }}</span>
<span
class="toggle-slider"
style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px;"
:style="{ backgroundColor: value ? '#4CAF50' : '#ccc' }"
@click="value = !value"
>
<span
class="toggle-knob"
style="position: absolute; content: ''; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%;"
:style="{ transform: value ? 'translateX(26px)' : 'translateX(0)' }"
></span>
</span>
</div>
<label :for="fieldId" class="checkbox-label" style="margin-left: 10px; cursor: pointer;">
{{ field.name }}
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
<span class="checkbox-description" style="display: block; font-size: 0.85em; color: #666;">
{{ field.description || '' }}
</span>
</label>
</div>
<!-- Geen beschrijving meer tonen, alleen als tooltip die al gedefinieerd is in de inputs -->
</div>
`
};

View File

@@ -0,0 +1,65 @@
// static/js/components/MaterialIconManager.js
/**
* Een hulpklasse om Material Symbols Outlined iconen te beheren
* en dynamisch toe te voegen aan de pagina indien nodig.
*/
export const MaterialIconManager = {
name: 'MaterialIconManager',
data() {
return {
loadedIconSets: [],
defaultOptions: {
opsz: 24, // Optimale grootte: 24px
wght: 400, // Gewicht: normaal
FILL: 0, // Vulling: niet gevuld
GRAD: 0 // Kleurverloop: geen
}
};
},
methods: {
/**
* Zorgt ervoor dat de Material Symbols Outlined stijlbladen zijn geladen
* @param {Object} options - Opties voor het icoon (opsz, wght, FILL, GRAD)
* @param {Array} iconNames - Optionele lijst met specifieke iconen om te laden
*/
ensureIconsLoaded(options = {}, iconNames = []) {
const opts = { ...this.defaultOptions, ...options };
const styleUrl = this.buildStyleUrl(opts, iconNames);
// Controleer of deze specifieke set al is geladen
if (!this.loadedIconSets.includes(styleUrl)) {
this.loadStylesheet(styleUrl);
this.loadedIconSets.push(styleUrl);
}
},
/**
* Bouwt de URL voor het stijlblad
*/
buildStyleUrl(options, iconNames = []) {
let url = `https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@${options.opsz},${options.wght},${options.FILL},${options.GRAD}`;
// Voeg specifieke iconNames toe als deze zijn opgegeven
if (iconNames.length > 0) {
url += `&icon_names=${iconNames.join(',')}`;
}
return url;
},
/**
* Laadt een stijlblad dynamisch
*/
loadStylesheet(url) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
console.log(`Material Symbols Outlined geladen: ${url}`);
}
}
};
// Singleton instantie om te gebruiken in de hele applicatie
export const iconManager = new Vue(MaterialIconManager);

View File

@@ -10,10 +10,6 @@ export const MessageHistory = {
type: Boolean,
default: false
},
formValues: {
type: Object,
default: () => ({})
},
isSubmittingForm: {
type: Boolean,
default: false
@@ -76,9 +72,6 @@ export const MessageHistory = {
}
},
handleSubmitForm(formData, messageId) {
this.$emit('submit-form', formData, messageId);
},
handleImageLoaded() {
// Auto-scroll when images load to maintain position
@@ -129,10 +122,8 @@ export const MessageHistory = {
<!-- The actual message -->
<chat-message
:message="message"
:form-values="formValues"
:is-submitting-form="isSubmittingForm"
:api-prefix="apiPrefix"
@submit-form="handleSubmitForm"
:api-prefix="apiPrefix"
@image-loaded="handleImageLoaded"
@specialist-complete="$emit('specialist-complete', $event)"
@specialist-error="$emit('specialist-error', $event)"
@@ -143,7 +134,6 @@ export const MessageHistory = {
<!-- Typing indicator -->
<typing-indicator v-if="isTyping"></typing-indicator>
</div>
</div>
`,
};

View File

@@ -189,24 +189,25 @@ export const ProgressTracker = {
console.log('Specialist Complete Data:', data);
// Extract answer from data.result.answer
if (data.result && data.result.answer) {
this.finalAnswer = data.result.answer;
if (data.result) {
if (data.result.answer) {
this.finalAnswer = data.result.answer;
console.log('Final Answer:', this.finalAnswer);
console.log('Final Answer:', this.finalAnswer);
// Direct update van de parent message als noodoplossing
try {
if (this.$parent && this.$parent.message) {
console.log('Direct update parent message');
this.$parent.message.content = data.result.answer;
// Direct update van de parent message als noodoplossing
try {
if (this.$parent && this.$parent.message) {
console.log('Direct update parent message');
this.$parent.message.content = data.result.answer;
}
} catch(err) {
console.error('Error updating parent message:', err);
}
} catch(err) {
console.error('Error updating parent message:', err);
}
// Emit event to parent met alle relevante data inclusief form_request
this.$emit('specialist-complete', {
answer: data.result.answer,
answer: data.result.answer || '',
form_request: data.result.form_request, // Voeg form_request toe
result: data.result,
interactionId: data.interaction_id,