tussentijdse status voor significante wijzigingen. Bezig aan creatie Dynamic Form in de chat client.

This commit is contained in:
Josako
2025-06-13 14:19:05 +02:00
parent b326c0c6f2
commit f1c60f9574
17 changed files with 1012 additions and 255 deletions

View File

@@ -408,22 +408,6 @@
}
/* Edit mode styling */
.edit-mode .edit-textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
min-height: 60px;
font-family: inherit;
margin-bottom: 8px;
}
.edit-actions {
display: flex;
gap: 8px;
}
.btn-small {
padding: 4px 12px;

View File

@@ -0,0 +1,75 @@
/* Stijlen voor het FormMessage component in chatberichten */
.form-message {
background-color: #f5f8ff;
border: 1px solid #e1e8f5;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.form-message-header {
display: flex;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e1e8f5;
}
.form-message-icon {
font-size: 16px;
margin-right: 8px;
color: #4a6fa5;
}
.form-message-title {
font-weight: 600;
color: #3a5a80;
font-size: 0.95rem;
}
.form-message-fields {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-message-field {
display: flex;
flex-wrap: wrap;
}
.field-message-label {
flex: 0 0 120px;
font-weight: 500;
color: #4a6fa5;
font-size: 0.85rem;
padding-right: 8px;
}
.field-message-value {
flex: 1;
min-width: 0;
font-size: 0.9rem;
color: #333;
word-break: break-word;
}
.field-message-value.text-value {
white-space: pre-wrap;
}
@media (max-width: 576px) {
.form-message-field {
flex-direction: column;
}
.field-message-label {
flex: 0 0 100%;
margin-bottom: 2px;
}
.field-message-value {
padding-left: 8px;
}
}

View File

@@ -0,0 +1,175 @@
/* 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;
}
.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;
}

View File

@@ -36,6 +36,7 @@ export const ChatApp = {
isSubmittingForm: false,
messageIdCounter: 1,
formValues: {},
currentInputFormData: null,
// API prefix voor endpoints
apiPrefix: chatConfig.apiPrefix || '',
@@ -298,6 +299,53 @@ export const ChatApp = {
}
},
async submitFormFromInput(formValues) {
this.isSubmittingForm = true;
if (!this.currentInputFormData) {
console.error('No form data available');
return;
}
console.log('Submitting form from input:', this.currentInputFormData.title, formValues);
try {
const response = await this.callAPI('/api/submit_form', {
formData: formValues,
formType: this.currentInputFormData.title,
conversation_id: this.conversationId,
user_id: this.userId
});
if (response.success) {
this.addMessage(
`${response.message || 'Formulier succesvol verzonden!'}`,
'ai',
'text'
);
// Wis het huidige formulier
this.currentInputFormData = null;
} else {
this.addMessage(
`❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`,
'ai',
'text'
);
}
} catch (error) {
console.error('Error submitting form:', error);
this.addMessage(
'Sorry, er ging iets mis bij het verzenden van het formulier. Probeer het opnieuw.',
'ai',
'text'
);
} finally {
this.isSubmittingForm = false;
}
},
// Message actions
retryMessage(messageId) {
@@ -387,6 +435,56 @@ export const ChatApp = {
this.filteredMessages = [];
},
// Event handlers voor specialist events
handleSpecialistComplete(eventData) {
console.log('ChatApp received specialist-complete:', eventData);
// Als er een form_request is, voeg deze toe als nieuw bericht
if (eventData.form_request) {
console.log('Adding form request as new message:', eventData.form_request);
// Converteer de form_request naar het verwachte formaat
const formData = this.convertFormRequest(eventData.form_request);
// Voeg het formulier toe als een nieuw AI bericht
this.addMessage('', 'ai', 'form', formData);
}
},
handleSpecialistError(eventData) {
console.log('ChatApp received specialist-error:', eventData);
// Voeg foutbericht toe
this.addMessage(
eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.',
'ai',
'error'
);
},
// Helper methode om form_request te converteren naar het verwachte formaat
convertFormRequest(formRequest) {
console.log('Converting form request:', formRequest);
// Converteer de fields van object naar array formaat
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
id: fieldId,
name: fieldDef.name,
type: fieldDef.type,
description: fieldDef.description,
required: fieldDef.required || false,
defaultValue: fieldDef.default || '',
allowedValues: fieldDef.allowed_values || null
}));
return {
title: formRequest.name,
icon: formRequest.icon || 'form',
version: formRequest.version || '1.0',
fields: fieldsArray
};
},
// Event handlers
handleResize() {
this.isMobile = window.innerWidth <= 768;
@@ -450,11 +548,54 @@ export const ChatApp = {
this.$refs.searchInput?.focus();
},
handleSpecialistError(errorData) {
console.error('Specialist error:', errorData);
// Als we willen kunnen we hier nog extra logica toevoegen, zoals statistieken bijhouden of centraal loggen
},
handleSpecialistComplete(eventData) {
console.log('ChatApp received specialist-complete:', eventData);
// Als er een form_request is, stuur deze naar de ChatInput component
if (eventData.form_request) {
console.log('Providing form request to ChatInput:', eventData.form_request);
// Converteer de form_request naar het verwachte formaat
const formData = this.convertFormRequest(eventData.form_request);
// Update de currentInputFormData voor ChatInput
this.currentInputFormData = formData;
}
},
handleSpecialistError(eventData) {
console.log('ChatApp received specialist-error:', eventData);
// Voeg foutbericht toe
this.addMessage(
eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.',
'ai',
'error'
);
},
// Helper methode om form_request te converteren naar het verwachte formaat
convertFormRequest(formRequest) {
console.log('Converting form request:', formRequest);
// Converteer de fields van object naar array formaat
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
id: fieldId,
name: fieldDef.name,
type: fieldDef.type,
description: fieldDef.description,
required: fieldDef.required || false,
defaultValue: fieldDef.default || '',
allowedValues: fieldDef.allowed_values || null
}));
return {
title: formRequest.name,
icon: formRequest.icon || 'form',
version: formRequest.version || '1.0',
fields: fieldsArray
};
},
},
template: `
@@ -469,6 +610,7 @@ export const ChatApp = {
:auto-scroll="true"
@submit-form="submitForm"
@specialist-error="handleSpecialistError"
@specialist-complete="handleSpecialistComplete"
ref="messageHistory"
class="chat-messages-area"
></message-history>
@@ -480,10 +622,12 @@ export const ChatApp = {
:max-length="2000"
:allow-file-upload="true"
:allow-voice-message="false"
:form-data="currentInputFormData"
@send-message="sendMessage"
@update-message="updateCurrentMessage"
@upload-file="handleFileUpload"
@record-voice="handleVoiceRecord"
@submit-form="submitFormFromInput"
ref="chatInput"
class="chat-input-area"
></chat-input>
@@ -493,12 +637,21 @@ export const ChatApp = {
};
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Zorg ervoor dat alle componenten correct geïnitialiseerd zijn voordat ze worden gebruikt
const initializeApp = () => {
console.log('Initializing Chat Application');
// Get access to the existing Vue app instance
if (window.__vueApp) {
// Zorg ervoor dat alle componenten globaal beschikbaar zijn via window
window.TypingIndicator = TypingIndicator;
window.FormField = FormField;
window.DynamicForm = DynamicForm;
window.ChatMessage = ChatMessage;
window.MessageHistory = MessageHistory;
window.ChatInput = ChatInput;
window.ProgressTracker = ProgressTracker;
// Register ALL components globally
window.__vueApp.component('TypingIndicator', TypingIndicator);
window.__vueApp.component('FormField', FormField);
@@ -520,4 +673,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
console.error('No existing Vue instance found on window.__vueApp');
}
});
};
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', initializeApp);

View File

@@ -2,6 +2,9 @@
export const ChatInput = {
name: 'ChatInput',
components: {
'dynamic-form': window.__vueApp ? DynamicForm : null
},
props: {
currentMessage: {
type: String,
@@ -19,11 +22,38 @@ export const ChatInput = {
type: Number,
default: 2000
},
formData: {
type: Object,
default: null
},
},
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;
}
},
immediate: true
},
currentMessage(newVal) {
this.localMessage = newVal;
},
localMessage(newVal) {
this.$emit('update-message', newVal);
this.autoResize();
}
},
emits: ['send-message', 'update-message'],
data() {
return {
localMessage: this.currentMessage,
formValues: {},
showForm: false
};
},
computed: {
@@ -35,19 +65,21 @@ export const ChatInput = {
return this.characterCount > this.maxLength;
},
canSend() {
return this.localMessage.trim() &&
!this.isLoading &&
!this.isOverLimit;
}
},
watch: {
currentMessage(newVal) {
this.localMessage = newVal;
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));
},
localMessage(newVal) {
this.$emit('update-message', newVal);
this.autoResize();
canSend() {
const hasValidForm = this.showForm && this.formData && this.validateForm();
const hasValidMessage = this.localMessage.trim() && !this.isOverLimit;
return (!this.isLoading) && (hasValidForm || hasValidMessage);
},
sendButtonText() {
return this.showForm ? 'Verstuur formulier' : 'Verstuur bericht';
}
},
mounted() {
@@ -64,11 +96,51 @@ export const ChatInput = {
},
sendMessage() {
if (this.canSend) {
if (!this.canSend) return;
if (this.showForm && 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
this.$emit('send-message');
}
},
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;
@@ -86,10 +158,44 @@ export const ChatInput = {
clearInput() {
this.localMessage = '';
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">
<dynamic-form
:form-data="formData"
:form-values="formValues"
:is-submitting="isLoading"
:hide-actions="true"
@update:form-values="updateFormValues"
></dynamic-form>
</div>
<div class="chat-input">
<!-- Main input area -->
<div class="input-main">
@@ -104,21 +210,34 @@ export const ChatInput = {
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">
<!-- 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>
<!-- Send button -->
<button
@click="sendMessage"
class="send-btn"
:class="{ 'form-mode': showForm && formData }"
:disabled="!canSend"
:title="isOverLimit ? 'Bericht te lang' : 'Bericht verzenden'"
:title="showForm ? '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

@@ -24,8 +24,6 @@ export const ChatMessage = {
emits: ['submit-form', 'image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
data() {
return {
isEditing: false,
editedContent: ''
};
},
methods: {
@@ -75,22 +73,6 @@ export const ChatMessage = {
.replace(/\n/g, '<br>');
},
startEdit() {
this.editedContent = this.message.content;
this.isEditing = true;
},
saveEdit() {
// Implementatie van bewerkingen zou hier komen
this.message.content = this.editedContent;
this.isEditing = false;
},
cancelEdit() {
this.isEditing = false;
this.editedContent = '';
},
submitForm() {
this.$emit('submit-form', this.message.formData, this.message.id);
},
@@ -125,23 +107,8 @@ export const ChatMessage = {
@specialist-error="handleSpecialistError"
></progress-tracker>
<!-- Edit mode -->
<div v-if="isEditing" class="edit-mode">
<textarea
v-model="editedContent"
class="edit-textarea"
rows="3"
@keydown.enter.ctrl="saveEdit"
@keydown.escape="cancelEdit"
></textarea>
<div class="edit-actions">
<button @click="saveEdit" class="btn-small btn-primary">Opslaan</button>
<button @click="cancelEdit" class="btn-small btn-secondary">Annuleren</button>
</div>
</div>
<!-- View mode -->
<div v-else>
<div>
<div
v-html="formatMessage(message.content)"
class="message-text"

View File

@@ -5,64 +5,173 @@ export const DynamicForm = {
type: Object,
required: true,
validator: (formData) => {
return formData.title && formData.fields && Array.isArray(formData.fields);
return formData && formData.title && formData.fields &&
(Array.isArray(formData.fields) || typeof formData.fields === 'object');
}
},
formValues: {
type: Object,
required: true
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 }
};
},
watch: {
formValues: {
handler(newValues) {
this.localFormValues = { ...newValues };
},
deep: true
},
localFormValues: {
handler(newValues) {
this.$emit('update:formValues', newValues);
},
deep: true
}
},
emits: ['submit', 'cancel'],
methods: {
handleSubmit() {
// Basic validation
const requiredFields = this.formData.fields.filter(field => field.required);
const missingFields = requiredFields.filter(field => {
const value = this.formValues[field.name];
return !value || (typeof value === 'string' && !value.trim());
});
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) {
const fieldNames = missingFields.map(f => f.label).join(', ');
const fieldNames = missingFields.join(', ');
alert(`De volgende velden zijn verplicht: ${fieldNames}`);
return;
}
this.$emit('submit');
this.$emit('submit', this.localFormValues);
},
handleCancel() {
this.$emit('cancel');
},
updateFieldValue(fieldName, value) {
// Emit an update for reactive binding
this.$emit('update-field', fieldName, value);
updateFieldValue(fieldId, value) {
this.localFormValues[fieldId] = value;
}
},
template: `
<div class="dynamic-form">
<div class="form-title">{{ formData.title }}</div>
<div v-if="formData.description" class="form-description">
{{ formData.description }}
<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>
<div class="form-title">{{ formData.title }}</div>
</div>
<form @submit.prevent="handleSubmit" novalidate>
<form-field
v-for="field in formData.fields"
:key="field.name"
:field="field"
:model-value="formValues[field.name]"
@update:model-value="formValues[field.name] = $event"
></form-field>
<div class="form-actions">
<div v-if="readOnly" class="form-readonly">
<!-- Array-based fields -->
<template v-if="Array.isArray(formData.fields)">
<div v-for="field in formData.fields" :key="field.id || field.name" class="form-field-readonly">
<div class="field-label">{{ field.name }}:</div>
<div class="field-value">
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
{{ localFormValues[field.id || field.name] || field.default || '-' }}
</template>
<template v-else-if="field.type === 'boolean'">
{{ localFormValues[field.id || field.name] ? 'Ja' : 'Nee' }}
</template>
<template v-else-if="field.type === 'text'">
<div class="text-value">{{ localFormValues[field.id || field.name] || '-' }}</div>
</template>
<template v-else>
{{ localFormValues[field.id || field.name] || field.default || '-' }}
</template>
</div>
</div>
</template>
<!-- Object-based fields -->
<template v-else>
<div v-for="(field, fieldId) in formData.fields" :key="fieldId" class="form-field-readonly">
<div class="field-label">{{ field.name }}:</div>
<div class="field-value">
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
{{ localFormValues[fieldId] || field.default || '-' }}
</template>
<template v-else-if="field.type === 'boolean'">
{{ localFormValues[fieldId] ? 'Ja' : 'Nee' }}
</template>
<template v-else-if="field.type === 'text'">
<div class="text-value">{{ localFormValues[fieldId] || '-' }}</div>
</template>
<template v-else>
{{ localFormValues[fieldId] || field.default || '-' }}
</template>
</div>
</div>
</template>
</div>
<form v-else @submit.prevent="handleSubmit" novalidate>
<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-id="field.id || field.name"
:field="field"
:model-value="localFormValues[field.id || field.name]"
@update:model-value="localFormValues[field.id || field.name] = $event"
></form-field>
</template>
<template v-else>
<form-field
v-for="(field, fieldId) in formData.fields"
:key="fieldId"
:field-id="fieldId"
:field="field"
:model-value="localFormValues[fieldId]"
@update:model-value="localFormValues[fieldId] = $event"
></form-field>
</template>
</div>
<div class="form-actions" v-if="!hideActions">
<button
type="submit"
class="btn btn-primary"
@@ -72,7 +181,7 @@ export const DynamicForm = {
<span v-if="isSubmitting" class="spinner"></span>
{{ isSubmitting ? 'Verzenden...' : (formData.submitText || 'Versturen') }}
</button>
<button
type="button"
class="btn btn-secondary"
@@ -81,28 +190,6 @@ export const DynamicForm = {
>
{{ formData.cancelText || 'Annuleren' }}
</button>
<!-- Optional reset button -->
<button
v-if="formData.showReset"
type="reset"
class="btn btn-outline"
@click="$emit('reset')"
:disabled="isSubmitting"
>
Reset
</button>
</div>
<!-- Progress indicator for multi-step forms -->
<div v-if="formData.steps && formData.currentStep" class="form-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: (formData.currentStep / formData.steps * 100) + '%' }"
></div>
</div>
<small>Stap {{ formData.currentStep }} van {{ formData.steps }}</small>
</div>
</form>
</div>

View File

@@ -5,22 +5,53 @@ export const FormField = {
type: Object,
required: true,
validator: (field) => {
return field.name && field.type && field.label;
return field.name && field.type;
}
},
fieldId: {
type: String,
required: true
},
modelValue: {
default: ''
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) {
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',
'boolean': 'checkbox'
};
return typeMap[this.field.type] || this.field.type;
},
stepValue() {
return this.field.type === 'float' ? 'any' : 1;
},
description() {
return this.field.description || '';
}
},
methods: {
@@ -33,147 +64,83 @@ export const FormField = {
},
template: `
<div class="form-field">
<label>
{{ field.label }}
<label :for="fieldId">
{{ field.name }}
<span v-if="field.required" class="required">*</span>
</label>
<!-- Text/Email/Tel inputs -->
<!-- Tekstinvoer (string/str) -->
<input
v-if="['text', 'email', 'tel', 'url', 'password'].includes(field.type)"
:type="field.type"
v-if="fieldType === 'text'"
:id="fieldId"
type="text"
v-model="value"
:required="field.required"
:placeholder="field.placeholder || ''"
:maxlength="field.maxLength"
:minlength="field.minLength"
:title="description"
>
<!-- Number input -->
<!-- Numerieke invoer (int/float) -->
<input
v-if="field.type === 'number'"
v-if="fieldType === 'number'"
:id="fieldId"
type="number"
v-model.number="value"
:required="field.required"
:min="field.min"
:max="field.max"
:step="field.step || 1"
:step="stepValue"
:placeholder="field.placeholder || ''"
:title="description"
>
<!-- Date input -->
<input
v-if="field.type === 'date'"
type="date"
v-model="value"
:required="field.required"
:min="field.min"
:max="field.max"
>
<!-- Time input -->
<input
v-if="field.type === 'time'"
type="time"
v-model="value"
:required="field.required"
>
<!-- File input -->
<input
v-if="field.type === 'file'"
type="file"
@change="handleFileUpload"
:required="field.required"
:accept="field.accept"
:multiple="field.multiple"
>
<!-- Select dropdown -->
<select
v-if="field.type === 'select'"
v-model="value"
:required="field.required"
>
<option value="">{{ field.placeholder || 'Kies een optie' }}</option>
<option
v-for="option in field.options"
:key="option.value || option"
:value="option.value || option"
>
{{ option.label || option }}
</option>
</select>
<!-- Radio buttons -->
<div v-if="field.type === 'radio'" class="radio-group">
<label
v-for="option in field.options"
:key="option.value || option"
class="radio-label"
>
<input
type="radio"
:value="option.value || option"
v-model="value"
:required="field.required"
>
<span>{{ option.label || option }}</span>
</label>
</div>
<!-- Checkboxes -->
<div v-if="field.type === 'checkbox'" class="checkbox-group">
<label
v-for="option in field.options"
:key="option.value || option"
class="checkbox-label"
>
<input
type="checkbox"
:value="option.value || option"
v-model="value"
>
<span>{{ option.label || option }}</span>
</label>
</div>
<!-- Single checkbox -->
<label v-if="field.type === 'single-checkbox'" class="checkbox-label">
<input
type="checkbox"
v-model="value"
:required="field.required"
>
<span>{{ field.checkboxText || field.label }}</span>
</label>
<!-- Textarea -->
<!-- Tekstvlak (text) -->
<textarea
v-if="field.type === 'textarea'"
v-if="fieldType === 'textarea'"
:id="fieldId"
v-model="value"
:required="field.required"
:rows="field.rows || 3"
:placeholder="field.placeholder || ''"
:maxlength="field.maxLength"
:title="description"
></textarea>
<!-- Range slider -->
<div v-if="field.type === 'range'" class="range-field">
<input
type="range"
v-model.number="value"
:min="field.min || 0"
:max="field.max || 100"
:step="field.step || 1"
<!-- 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"
>
<span class="range-value">{{ value }}</span>
{{ 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>
</div>
<!-- Help text -->
<small v-if="field.helpText" class="help-text">
{{ field.helpText }}
</small>
<!-- Checkbox (boolean) -->
<div v-if="fieldType === 'checkbox'" class="checkbox-container">
<label class="checkbox-label">
<input
:id="fieldId"
type="checkbox"
v-model="value"
:required="field.required"
:title="description"
>
<span class="checkbox-text">{{ field.description || 'Ja' }}</span>
</label>
</div>
<!-- Geen beschrijving meer tonen, alleen als tooltip die al gedefinieerd is in de inputs -->
</div>
`
};

View File

@@ -0,0 +1,59 @@
// static/js/components/FormMessage.js
export const FormMessage = {
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
};
});
}
},
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>
`
};

View File

@@ -134,6 +134,8 @@ export const MessageHistory = {
:api-prefix="apiPrefix"
@submit-form="handleSubmitForm"
@image-loaded="handleImageLoaded"
@specialist-complete="$emit('specialist-complete', $event)"
@specialist-error="$emit('specialist-error', $event)"
></chat-message>
</template>
</template>

View File

@@ -204,9 +204,10 @@ export const ProgressTracker = {
console.error('Error updating parent message:', err);
}
// Emit event to parent met alle relevante data
// Emit event to parent met alle relevante data inclusief form_request
this.$emit('specialist-complete', {
answer: data.result.answer,
form_request: data.result.form_request, // Voeg form_request toe
result: data.result,
interactionId: data.interaction_id,
taskId: this.taskId