Chat client changes

- Form values shown correct in MessageHistory of Chat client
- Improements to CSS
- Move css en js to assets directory
- Introduce better Personal Contact Form & Professional Contact Form
- Start working on actual Selection Specialist
This commit is contained in:
Josako
2025-06-15 05:25:00 +02:00
parent 3c7460f741
commit 82e25b356c
27 changed files with 1077 additions and 161 deletions

View File

@@ -32,7 +32,6 @@
border-radius: 15px;
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
width: 100%;
max-width: 1000px; /* Optimale breedte */
@@ -377,7 +376,7 @@
/* User message bubble styling */
.message.user .message-content {
background: linear-gradient(135deg, #007bff, #0056b3);
background: rgba(0, 0, 0, 0.1);
color: white;
border-bottom-right-radius: 4px;
}
@@ -385,9 +384,8 @@
/* AI/Bot message bubble styling */
.message.ai .message-content,
.message.bot .message-content {
background: #f8f9fa;
background: rgba(255, 255, 255, 0.1);
color: #212529;
border: 1px solid #e9ecef;
border-bottom-left-radius: 4px;
margin-right: 60px;
}
@@ -670,7 +668,6 @@
/* Progress Tracker Styling */
.progress-tracker {
margin: 8px 0;
border: 1px solid #e9ecef;
border-radius: 8px;
background: #f8f9fa;
overflow: hidden;
@@ -683,8 +680,7 @@
}
.progress-tracker.completed {
border-color: #28a745;
background: #d4edda;
background: rgba(155, 255, 155, 0.1);
}
.progress-header {

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
/* Styling voor formulier in berichten */
.message .form-display {
margin-bottom: 12px;
border-radius: 8px;
background-color: rgba(245, 245, 245, 0.7);
padding: 12px;
border: 1px solid #e0e0e0;
}
.message.user .form-display {
background-color: rgba(255, 255, 255, 0.1);
}
.message.ai .form-display {
background-color: rgba(245, 245, 250, 0.7);
}
/* Styling voor formulieren in berichten */
.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%;
}

View File

@@ -170,21 +170,22 @@ export const ChatApp = {
},
// Message management
addMessage(content, sender, type = 'text', formData = null) {
addMessage(content, sender, type = 'text', formData = null, formValues = null) {
const message = {
id: this.messageIdCounter++,
content,
sender,
type,
formData,
formValues,
timestamp: new Date().toISOString(),
status: sender === 'user' ? 'sent' : 'delivered'
};
this.allMessages.push(message);
// Initialize form values if it's a form
if (type === 'form' && formData) {
// Initialize form values if it's a form and no values were provided
if (type === 'form' && formData && !formValues) {
// Vue 3 compatibele manier om reactieve objecten bij te werken
this.formValues[message.id] = {};
formData.fields.forEach(field => {
@@ -203,19 +204,32 @@ export const ChatApp = {
return message;
},
// Helper functie om formulierdata toe te voegen aan bestaande berichten
attachFormDataToMessage(messageId, formData, formValues) {
const message = this.allMessages.find(m => m.id === messageId);
if (message) {
message.formData = formData;
message.formValues = formValues;
}
},
updateCurrentMessage(value) {
this.currentMessage = value;
},
// Message sending
// Message sending (alleen voor gewone tekstberichten, geen formulieren)
async sendMessage() {
const text = this.currentMessage.trim();
// Controleer of we kunnen verzenden
if (!text || this.isLoading) return;
console.log('Sending message:', text);
console.log('Sending text message:', text);
// Add user message
const userMessage = this.addMessage(text, 'user', 'text');
// Wis input
this.currentMessage = '';
// Show typing and loading state
@@ -223,11 +237,14 @@ export const ChatApp = {
this.isLoading = true;
try {
const response = await this.callAPI('/api/send_message', {
// Verzamel gegevens voor de API call
const apiData = {
message: text,
conversation_id: this.conversationId,
user_id: this.userId
});
};
const response = await this.callAPI('/api/send_message', apiData);
// Hide typing indicator
this.isTyping = false;
@@ -278,35 +295,61 @@ export const ChatApp = {
return;
}
console.log('Submitting form from input:', this.currentInputFormData.title, formValues);
console.log('Form values received:', formValues);
console.log('Current input form data:', this.currentInputFormData);
try {
const response = await this.callAPI('/api/submit_form', {
formData: formValues,
formType: this.currentInputFormData.title,
// Maak een user message met formuliergegevens én eventuele tekst
const userMessage = this.addMessage(
this.currentMessage.trim(), // Voeg tekst toe als die er is
'user',
'text'
);
// Voeg formuliergegevens toe aan het bericht
userMessage.formData = this.currentInputFormData;
userMessage.formValues = formValues;
// Reset het tekstbericht
this.currentMessage = '';
this.$emit('update-message', '');
// Toon laad-indicator
this.isTyping = true;
this.isLoading = true;
// Verzamel gegevens voor de API call
const apiData = {
message: userMessage.content,
conversation_id: this.conversationId,
user_id: this.userId
});
user_id: this.userId,
form_values: formValues // Voeg formuliergegevens toe aan API call
};
if (response.success) {
this.addMessage(
`${response.message || 'Formulier succesvol verzonden!'}`,
'ai',
'text'
);
// Verstuur bericht naar de API
const response = await this.callAPI('/api/send_message', apiData);
// Wis het huidige formulier (ongeacht of het succesvol was of niet)
this.currentInputFormData = null;
} else {
this.addMessage(
`❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`,
'ai',
'text'
);
// Wis ook hier het formulier na een fout
this.currentInputFormData = null;
// Verberg de typing indicator
this.isTyping = false;
// Markeer het gebruikersbericht als afgeleverd
userMessage.status = 'delivered';
// Voeg AI response toe met task_id voor tracking
const aiMessage = this.addMessage(
'',
'ai',
'text'
);
if (response.task_id) {
console.log('Monitoring Task ID: ', response.task_id);
aiMessage.taskId = response.task_id;
}
// Reset formulier na succesvolle verzending
this.currentInputFormData = null;
} catch (error) {
console.error('Error submitting form:', error);
this.addMessage(
@@ -318,6 +361,7 @@ export const ChatApp = {
this.currentInputFormData = null;
} finally {
this.isSubmittingForm = false;
this.isLoading = false;
}
},

View File

@@ -125,9 +125,15 @@
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...';
@@ -181,20 +187,31 @@
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
// Verstuur het formulier, eventueel met aanvullende tekst
this.$emit('submit-form', this.formValues);
this.formValues = {};
}
} else if (this.localMessage.trim()) {
// Verstuur normaal bericht
// Verstuur normaal bericht zonder formulier
this.$emit('send-message');
}
},
// Deze methode houden we voor de volledigheid, maar zal niet meer direct worden aangeroepen
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
@@ -261,7 +278,7 @@
<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);">
<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

View File

@@ -1,3 +1,33 @@
// Voeg stylesheets toe voor formulier en chat berichten weergave
const addStylesheets = () => {
// Formulier stylesheet
if (!document.querySelector('link[href*="form-message.css"]')) {
const formLink = document.createElement('link');
formLink.rel = 'stylesheet';
formLink.href = '/static/assets/css/form-message.css';
document.head.appendChild(formLink);
}
// Chat bericht stylesheet
if (!document.querySelector('link[href*="chat-message.css"]')) {
const chatLink = document.createElement('link');
chatLink.rel = 'stylesheet';
chatLink.href = '/static/assets/css/chat-message.css';
document.head.appendChild(chatLink);
}
// Material Icons font stylesheet
if (!document.querySelector('link[href*="Material+Symbols+Outlined"]')) {
const iconLink = document.createElement('link');
iconLink.rel = 'stylesheet';
iconLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0';
document.head.appendChild(iconLink);
}
};
// Laad de stylesheets
addStylesheets();
export const ChatMessage = {
name: 'ChatMessage',
props: {
@@ -17,11 +47,38 @@ export const ChatMessage = {
default: ''
}
},
created() {
// Zorg ervoor dat het icoon geladen wordt als iconManager beschikbaar is
if (window.iconManager && this.message.formData && this.message.formData.icon) {
window.iconManager.loadIcon(this.message.formData.icon);
}
},
watch: {
'message.formData.icon': {
handler(newIcon) {
if (newIcon && window.iconManager) {
window.iconManager.loadIcon(newIcon);
}
},
immediate: true
}
},
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
data() {
return {
formVisible: true
};
},
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;
}
},
methods: {
handleSpecialistError(eventData) {
console.log('ChatMessage received specialist-error event:', eventData);
@@ -90,7 +147,7 @@ export const ChatMessage = {
<div :class="getMessageClass()" :data-message-id="message.id">
<!-- Normal text messages -->
<template v-if="message.type === 'text'">
<div class="message-content">
<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"
@@ -101,9 +158,79 @@ export const ChatMessage = {
@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>
</td>
</tr>
</tbody>
</table>
</div>
<!-- View mode -->
<div>
<div
v-if="message.content"
v-html="formatMessage(message.content)"
class="message-text"
></div>

View File

@@ -151,12 +151,12 @@ export const DynamicForm = {
</div>
</div>
<div v-if="readOnly" class="form-readonly">
<div v-if="readOnly" class="form-readonly" style="display: grid; grid-template-columns: 35% 65%; gap: 8px; width: 100%;">
<!-- 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-for="field in formData.fields" :key="field.id || field.name">
<div class="field-label" style="font-weight: 500; color: #555; padding: 4px 0;">{{ field.name }}:</div>
<div class="field-value" style="padding: 4px 0;">
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
{{ localFormValues[field.id || field.name] || field.default || '-' }}
</template>
@@ -164,19 +164,19 @@ export const DynamicForm = {
{{ 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>
<div class="text-value" style="white-space: pre-wrap;">{{ localFormValues[field.id || field.name] || '-' }}</div>
</template>
<template v-else>
{{ localFormValues[field.id || field.name] || field.default || '-' }}
</template>
</div>
</div>
</template>
</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-for="(field, fieldId) in formData.fields" :key="fieldId">
<div class="field-label" style="font-weight: 500; color: #555; padding: 4px 0;">{{ field.name }}:</div>
<div class="field-value" style="padding: 4px 0;">
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
{{ localFormValues[fieldId] || field.default || '-' }}
</template>
@@ -184,13 +184,13 @@ export const DynamicForm = {
{{ localFormValues[fieldId] ? 'Ja' : 'Nee' }}
</template>
<template v-else-if="field.type === 'text'">
<div class="text-value">{{ localFormValues[fieldId] || '-' }}</div>
<div class="text-value" style="white-space: pre-wrap;">{{ localFormValues[fieldId] || '-' }}</div>
</template>
<template v-else>
{{ localFormValues[fieldId] || field.default || '-' }}
</template>
</div>
</div>
</template>
</template>
</div>

View File

@@ -1,11 +0,0 @@
/* Animation styles for ChatInput component */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Add more ChatInput-specific styles here */
.loading-spinner {
display: inline-block;
animation: spin 1s linear infinite;
}

View File

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