- Refinement of the chat client to have better visible clues for user vs chatbot messages
- Introduction of interview_phase and normal phase in TRAICIE_SELECTION_SPECIALIST to make interaction with bot more human. - More and random humanised messages to TRAICIE_SELECTION_SPECIALIST
This commit is contained in:
@@ -79,23 +79,14 @@
|
||||
}
|
||||
|
||||
/* Chat Input styling */
|
||||
.chat-input-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 20px; /* Interne padding voor ChatInput */
|
||||
box-sizing: border-box;
|
||||
max-width: 1000px; /* Optimale breedte */
|
||||
margin-left: auto;
|
||||
margin-right: auto; /* Horizontaal centreren */
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
@@ -113,37 +104,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background: var(--active-text-color);
|
||||
color: var(--active-background-color);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
/* Character counter */
|
||||
.character-counter {
|
||||
position: absolute;
|
||||
@@ -152,7 +112,7 @@
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
@@ -190,11 +150,6 @@
|
||||
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 15px;
|
||||
max-width: 100%; /* Op mobiel volledige breedte gebruiken */
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 15px;
|
||||
gap: 10px;
|
||||
@@ -224,10 +179,6 @@
|
||||
.message-history-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
@@ -343,16 +294,16 @@
|
||||
|
||||
/* User message bubble styling */
|
||||
.message.user .message-content {
|
||||
background: var(--history-user-message-background);
|
||||
color: var(--history-message-text-color);
|
||||
background: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* AI/Bot message bubble styling */
|
||||
.message.ai .message-content,
|
||||
.message.bot .message-content {
|
||||
background: var(--history-ai-message-background);
|
||||
color: var(--history-message-text-color);
|
||||
background: var(--ai-message-background);
|
||||
color: var(--ai-message-text-color);
|
||||
border-bottom-left-radius: 4px;
|
||||
margin-right: 60px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
/* 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 {
|
||||
@@ -42,38 +34,6 @@
|
||||
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;
|
||||
@@ -85,3 +45,40 @@
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Form actions container */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Form send actions container - for send button within form */
|
||||
.form-send-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Dynamic form container transitions */
|
||||
.dynamic-form-container {
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Chat input transitions */
|
||||
.chat-input {
|
||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Smooth transitions for mode switching */
|
||||
.chat-input-container > * {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
@@ -149,3 +149,46 @@
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Ensure forms in messages use full available width */
|
||||
.message .dynamic-form-container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.message .dynamic-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message .form-fields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Optimize form field layout in messages to prevent unnecessary label wrapping */
|
||||
.message .form-field {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 70%;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure form field inputs use full available space */
|
||||
.message .form-field input,
|
||||
.message .form-field select,
|
||||
.message .form-field textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Special handling for radio fields in messages */
|
||||
.message .form-field.radio-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message .form-field.radio-field .field-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -124,3 +124,21 @@
|
||||
.text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Loading spinner for send button */
|
||||
.form-actions .loading-spinner {
|
||||
display: inline-block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Flexbox layout for single send button */
|
||||
.form-actions.with-send-button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
BIN
eveai_chat_client/static/assets/img/eveai_logo.png
Normal file
BIN
eveai_chat_client/static/assets/img/eveai_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
21
eveai_chat_client/static/assets/img/eveai_logo.svg
Normal file
21
eveai_chat_client/static/assets/img/eveai_logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Laag_1" data-name="Laag 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 360">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #99498f;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #9c8ae1;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #eb7f31;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M303.75,187.38c-.39,21.63-23.09,71.46-44.05,56.75-7.01-4.92-9.52-14.14-6.4-22.11,46.36-118.47-110.59-153.63-165.22-81.96-3.2,4.19-7.98,6.92-13.23,7.39-29.86,2.67-11.81-28.09,9.75-54.84.31-.38.63-.75.96-1.11,77.63-83.29,223.16-15.54,218.2,95.89Z"/>
|
||||
<path class="cls-3" d="M309.51,261.49c.78,11.39-3.07,22.02-9.91,29.99-6.85,7.97-16.7,13.29-27.94,14.06-5.31.36-10.46-.32-15.24-1.88-6.99-2.28-8.45-11.54-2.54-15.91,5.59-4.13,10.81-8.72,15.62-13.73,1.58-1.64,3.12-3.33,4.61-5.07,1.49-1.73,2.94-3.51,4.33-5.33,2.91-3.79,5.6-7.75,8.07-11.88,1.06-1.77,2.07-3.57,3.05-5.39,3.49-6.55,13.05-6.36,16.17.37,2.1,4.52,3.42,9.49,3.78,14.77Z"/>
|
||||
<path class="cls-2" d="M248.52,246.01c-17.19-10.96-6.79-24.59-5.31-32.5,21.64-115.53-112.72-111.33-150.58-59.62-5.74,7.84-24.59,5.08-25.04,5.03-8.37-.78-11.67,1.66-14,10.54-27.79,106.04,132.76,184.42,198.64,101.39,6.27-7.9,4.79-19.42-3.71-24.84ZM95.86,233.32c-29.22.43-29.22-45.47,0-45.04,29.21-.43,29.21,45.47,0,45.04ZM189.87,233.32c-29.21.43-29.21-45.47,0-45.04,29.22-.43,29.22,45.47,0,45.04Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
eveai_chat_client/static/assets/img/eveai_logo.webp
Normal file
BIN
eveai_chat_client/static/assets/img/eveai_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
BIN
eveai_chat_client/static/assets/img/favicon.png
Normal file
BIN
eveai_chat_client/static/assets/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -26,14 +26,16 @@
|
||||
:form-values="formValues"
|
||||
:api-prefix="apiPrefix"
|
||||
:is-submitting="isLoading"
|
||||
:hide-actions="true"
|
||||
:show-send-button="true"
|
||||
:is-submitting-form="isLoading"
|
||||
:send-button-text="'Verstuur formulier'"
|
||||
@update:form-values="updateFormValues"
|
||||
@form-enter-pressed="sendMessage"
|
||||
@form-send-submit="handleFormSendSubmit"
|
||||
></dynamic-form>
|
||||
|
||||
<!-- Geen extra knoppen meer onder het formulier, alles gaat via de hoofdverzendknop -->
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div v-if="!formData" class="chat-input">
|
||||
<!-- Main input area -->
|
||||
<div class="input-main">
|
||||
<textarea
|
||||
@@ -57,13 +59,12 @@
|
||||
<!-- Input actions -->
|
||||
<div class="input-actions">
|
||||
|
||||
<!-- Universele verzendknop (voor zowel berichten als formulieren) -->
|
||||
<!-- Message send button -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="send-btn"
|
||||
:class="{ 'form-mode': formData }"
|
||||
:disabled="!canSend"
|
||||
:title="formData ? 'Verstuur formulier' : 'Verstuur bericht'"
|
||||
:title="'Verstuur bericht'"
|
||||
>
|
||||
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -156,12 +157,15 @@ export default {
|
||||
},
|
||||
|
||||
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);
|
||||
if (this.isLoading) return false;
|
||||
|
||||
if (this.formData) {
|
||||
// Form mode: only validate form, message is optional
|
||||
return this.validateForm();
|
||||
} else {
|
||||
// Normal mode: validate message
|
||||
return this.localMessage.trim() && !this.isOverLimit;
|
||||
}
|
||||
},
|
||||
|
||||
hasFormDataToSend() {
|
||||
@@ -319,21 +323,32 @@ export default {
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
console.log('ChatInput: sendMessage called, formData:', !!this.formData);
|
||||
if (!this.canSend) return;
|
||||
|
||||
// Bij een formulier gaan we het formulier en optioneel bericht combineren
|
||||
if (this.formData) {
|
||||
console.log('ChatInput: Processing form submission');
|
||||
// Valideer het formulier
|
||||
if (this.validateForm()) {
|
||||
// Verstuur het formulier, eventueel met aanvullende tekst
|
||||
this.$emit('submit-form', this.formValues);
|
||||
}
|
||||
} else if (this.localMessage.trim()) {
|
||||
console.log('ChatInput: Processing regular message');
|
||||
// Verstuur normaal bericht zonder formulier
|
||||
this.$emit('send-message');
|
||||
}
|
||||
},
|
||||
|
||||
handleFormSendSubmit(formValues) {
|
||||
console.log('ChatInput: handleFormSendSubmit called with values:', formValues);
|
||||
// Zorg dat formValues correct worden doorgegeven
|
||||
this.formValues = formValues;
|
||||
// Roep sendMessage aan om de normale flow te volgen
|
||||
this.sendMessage();
|
||||
},
|
||||
|
||||
getFormValuesForSending() {
|
||||
// Geeft de huidige formulierwaarden terug voor verzending
|
||||
return this.formValues;
|
||||
@@ -420,9 +435,9 @@ export default {
|
||||
/* Algemene container */
|
||||
.chat-input-container {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
color: var(--human-message-text-color);
|
||||
border-top: 1px solid #e0e0e0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
@@ -454,8 +469,8 @@ export default {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
/* Transparante achtergrond in plaats van wit */
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background-color: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
/* Box-sizing om padding correct te berekenen */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -466,7 +481,8 @@ export default {
|
||||
right: 15px;
|
||||
bottom: 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: var(--human-message-text-color);
|
||||
opacity: 0.7;
|
||||
pointer-events: none; /* Voorkom dat deze de textarea verstoort */
|
||||
}
|
||||
|
||||
@@ -491,38 +507,24 @@ export default {
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
border: 1px solid var(--active-text-color);
|
||||
background-color: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
border: 2px solid var(--human-message-text-color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
flex-shrink: 0; /* Voorkom dat de knop krimpt */
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
background-color: var(--active-text-color);
|
||||
color: var(--active-background-color);
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background-color: var(--human-message-background);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background-color: #ccc;
|
||||
color: #666;
|
||||
border-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-btn.form-mode {
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
border-color: var(--active-text-color);
|
||||
}
|
||||
|
||||
.send-btn.form-mode:hover {
|
||||
background-color: var(--active-text-color);
|
||||
color: var(--active-background-color);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
@@ -538,8 +540,8 @@ export default {
|
||||
.active-ai-message-area {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background-color: var(--ai-message-background);
|
||||
color: var(--ai-message-text-color);
|
||||
border-radius: 8px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
<!-- Normal text messages -->
|
||||
<template v-if="message.type === 'text'">
|
||||
<div class="message-content" style="width: 100%;">
|
||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||
<img
|
||||
v-if="message.sender === 'ai'"
|
||||
src="/static/assets/img/eveai_logo.svg"
|
||||
alt="EveAI"
|
||||
class="ai-message-logo"
|
||||
/>
|
||||
<!-- Voortgangstracker voor AI berichten met task_id - ALLEEN VOOR LAATSTE AI MESSAGE -->
|
||||
<progress-tracker
|
||||
v-if="message.sender === 'ai' && message.taskId && isLatestAiMessage"
|
||||
@@ -95,6 +102,13 @@
|
||||
<!-- Error messages -->
|
||||
<template v-else-if="message.type === 'error'">
|
||||
<div class="message-content error-content">
|
||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||
<img
|
||||
v-if="message.sender === 'ai'"
|
||||
src="/static/assets/img/eveai_logo.svg"
|
||||
alt="EveAI"
|
||||
class="ai-message-logo"
|
||||
/>
|
||||
<div class="form-error">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
@@ -107,6 +121,13 @@
|
||||
<!-- Other message types -->
|
||||
<template v-else>
|
||||
<div class="message-content">
|
||||
<!-- EveAI Logo voor AI berichten - links boven, half buiten de bubbel -->
|
||||
<img
|
||||
v-if="message.sender === 'ai'"
|
||||
src="/static/assets/img/eveai_logo.svg"
|
||||
alt="EveAI"
|
||||
class="ai-message-logo"
|
||||
/>
|
||||
<div class="message-text" v-html="formatMessage(message.content)"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -304,6 +325,11 @@ export default {
|
||||
getMessageClass() {
|
||||
let classes = `message ${this.message.sender}`;
|
||||
|
||||
// Add 'has-form' class for user messages with formulieren
|
||||
if (this.message.sender === 'user' && this.hasMeaningfulFormValues(this.message)) {
|
||||
classes += ' has-form';
|
||||
}
|
||||
|
||||
// Add class for temporarily positioned AI messages
|
||||
if (this.message.isTemporarilyAtBottom) {
|
||||
classes += ' temporarily-at-bottom';
|
||||
@@ -343,10 +369,16 @@ export default {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* User messages with forms get fixed width of 90% */
|
||||
.message.user.has-form {
|
||||
width: 90%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Styling for temporarily positioned AI messages */
|
||||
.message.ai.temporarily-at-bottom {
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background-color: var(--ai-message-background);
|
||||
color: var(--ai-message-text-color);
|
||||
opacity: 0.9;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
@@ -355,24 +387,24 @@ export default {
|
||||
|
||||
/* Styling for messages in sticky area - override history colors with active colors */
|
||||
.message.sticky-area .message-content {
|
||||
background: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background: var(--ai-message-background);
|
||||
color: var(--ai-message-text-color);
|
||||
}
|
||||
|
||||
/* Override message bubble colors for sticky area */
|
||||
.message.sticky-area.user .message-content,
|
||||
.message.sticky-area.ai .message-content {
|
||||
background: var(--active-background-color) !important;
|
||||
color: var(--active-text-color) !important;
|
||||
border: 1px solid var(--active-text-color);
|
||||
background: var(--ai-message-background) !important;
|
||||
color: var(--ai-message-text-color) !important;
|
||||
border: 1px solid var(--ai-message-text-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Active styling for messages in input area */
|
||||
.message.input-area .message-content {
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background-color: var(--ai-message-background);
|
||||
color: var(--ai-message-text-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -382,10 +414,30 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* EveAI Logo styling voor AI berichten */
|
||||
.ai-message-logo {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -20px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--ai-message-background);
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Ensure message-content has relative positioning for logo positioning */
|
||||
.message.ai .message-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Formulier styling */
|
||||
.form-display {
|
||||
margin: 15px 0;
|
||||
color: var(--active-text-color);
|
||||
color: var(--human-message-text-color);
|
||||
padding: 15px;
|
||||
font-family: inherit;
|
||||
}
|
||||
@@ -425,11 +477,11 @@ export default {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--active-text-color);
|
||||
border: 1px solid var(--human-message-text-color);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
background-color: var(--active-background-color);
|
||||
color: var(--active-text-color);
|
||||
background-color: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
}
|
||||
|
||||
.form-result-table textarea.form-textarea {
|
||||
@@ -548,6 +600,11 @@ export default {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
/* User messages with forms get fixed width of 95% on mobile */
|
||||
.message.user.has-form {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.form-result-table td:first-child {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
@update:model-value="updateFieldValue(field.id || field.name, $event)"
|
||||
@open-privacy-modal="openPrivacyModal"
|
||||
@open-terms-modal="openTermsModal"
|
||||
@keydown-enter="handleEnterKey"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="typeof formData.fields === 'object'">
|
||||
@@ -33,29 +34,49 @@
|
||||
@update:model-value="updateFieldValue(fieldId, $event)"
|
||||
@open-privacy-modal="openPrivacyModal"
|
||||
@open-terms-modal="openTermsModal"
|
||||
@keydown-enter="handleEnterKey"
|
||||
/>
|
||||
</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 v-if="!hideActions && !readOnly" class="form-actions" :class="{ 'with-send-button': showSendButton }">
|
||||
<!-- Send button mode (ChatInput styling) -->
|
||||
<template v-if="showSendButton">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSendSubmit"
|
||||
class="send-btn"
|
||||
:disabled="isSubmittingForm || !isFormValid"
|
||||
:title="sendButtonText"
|
||||
>
|
||||
<span v-if="isSubmittingForm" 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>
|
||||
</template>
|
||||
|
||||
<!-- Standard buttons mode -->
|
||||
<template v-else>
|
||||
<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>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Read-only form display -->
|
||||
@@ -66,8 +87,9 @@
|
||||
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 class="field-value" :class="{'text-value': field.type === 'text', 'boolean-value': field.type === 'boolean'}">
|
||||
<span v-if="field.type === 'boolean'" v-html="formatFieldValue(fieldId, field)"></span>
|
||||
<span v-else>{{ formatFieldValue(fieldId, field) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,12 +109,15 @@ export default {
|
||||
'form-field': FormField
|
||||
},
|
||||
setup(props) {
|
||||
const { watchIcon } = useIconManager();
|
||||
const { watchIcon, loadIcons } = useIconManager();
|
||||
const contentModal = injectContentModal();
|
||||
|
||||
// Watch formData.icon for automatic icon loading
|
||||
watchIcon(() => props.formData?.icon);
|
||||
|
||||
// Preload boolean icons
|
||||
loadIcons(['check_circle', 'cancel']);
|
||||
|
||||
return {
|
||||
contentModal
|
||||
};
|
||||
@@ -149,9 +174,21 @@ export default {
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showSendButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sendButtonText: {
|
||||
type: String,
|
||||
default: 'Verstuur formulier'
|
||||
},
|
||||
isSubmittingForm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['submit', 'cancel', 'update:formValues'],
|
||||
emits: ['submit', 'cancel', 'update:formValues', 'form-enter-pressed', 'form-send-submit'],
|
||||
data() {
|
||||
return {
|
||||
localFormValues: { ...this.formValues }
|
||||
@@ -259,6 +296,11 @@ export default {
|
||||
mounted() {
|
||||
// Proactief alle boolean velden initialiseren bij het laden
|
||||
this.initializeBooleanFields();
|
||||
|
||||
// Auto-focus on first form field for better UX
|
||||
this.$nextTick(() => {
|
||||
this.focusFirstField();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// Proactieve initialisatie van alle boolean velden
|
||||
@@ -388,6 +430,16 @@ export default {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
handleSendSubmit() {
|
||||
// Eerst proactief alle boolean velden corrigeren
|
||||
this.initializeBooleanFields();
|
||||
|
||||
// Wacht tot updates zijn verwerkt, dan emit de form values
|
||||
this.$nextTick(() => {
|
||||
this.$emit('form-send-submit', this.localFormValues);
|
||||
});
|
||||
},
|
||||
|
||||
getFieldsForDisplay() {
|
||||
// Voor read-only weergave
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
@@ -410,7 +462,15 @@ export default {
|
||||
|
||||
// Format different field types
|
||||
if (field.type === 'boolean') {
|
||||
return value ? true : false;
|
||||
const iconName = value ? 'check_circle' : 'cancel';
|
||||
const label = value ? 'Ja' : 'Nee';
|
||||
const cssClass = value ? 'boolean-true' : 'boolean-false';
|
||||
|
||||
return `<span class="material-symbols-outlined boolean-icon ${cssClass}"
|
||||
aria-label="${label}"
|
||||
title="${label}">
|
||||
${iconName}
|
||||
</span>`;
|
||||
} else if (field.type === 'enum' && !value && field.default) {
|
||||
return field.default;
|
||||
}
|
||||
@@ -450,6 +510,26 @@ export default {
|
||||
title: title,
|
||||
contentUrl: contentUrl
|
||||
});
|
||||
},
|
||||
|
||||
// Handle Enter key press in form fields
|
||||
handleEnterKey(event) {
|
||||
console.log('DynamicForm: Enter event received, emitting form-enter-pressed');
|
||||
// Prevent default form submission
|
||||
event.preventDefault();
|
||||
// Emit event to parent (ChatInput) to trigger send
|
||||
this.$emit('form-enter-pressed');
|
||||
},
|
||||
|
||||
// Focus management - auto-focus on first form field
|
||||
focusFirstField() {
|
||||
if (this.readOnly) return; // Don't focus in read-only mode
|
||||
|
||||
// Find the first focusable input element
|
||||
const firstInput = this.$el.querySelector('input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), textarea, select');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -463,6 +543,8 @@ export default {
|
||||
}
|
||||
|
||||
.dynamic-form {
|
||||
background: var(--human-message-background);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
@@ -472,11 +554,11 @@ export default {
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--active-text-color);
|
||||
border-bottom: 1px solid var(--human-message-text-color);
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .form-header {
|
||||
border-bottom: 1px solid var(--history-message-text-color);
|
||||
border-bottom: 1px solid #777;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
@@ -486,21 +568,21 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--active-text-color);
|
||||
color: var(--human-message-text-color);
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .form-icon {
|
||||
color: var(--history-message-text-color);
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--active-text-color);
|
||||
color: var(--human-message-text-color);
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .form-title {
|
||||
color: var(--history-message-text-color);
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
@@ -650,7 +732,7 @@ export default {
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .form-field-readonly {
|
||||
border-bottom: 1px solid var(--history-message-text-color);
|
||||
border-bottom: 1px solid #777;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@@ -659,20 +741,73 @@ export default {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .field-label {
|
||||
color: var(--history-message-text-color);
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dynamic-form.readonly .field-value {
|
||||
color: var(--history-message-text-color);
|
||||
}
|
||||
|
||||
.text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Boolean icon styling */
|
||||
.boolean-icon {
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.boolean-true {
|
||||
color: #4caf50; /* Groen voor true */
|
||||
}
|
||||
|
||||
.boolean-false {
|
||||
color: #f44336; /* Rood voor false */
|
||||
}
|
||||
|
||||
.field-value.boolean-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Send button styling (ChatInput consistency) */
|
||||
.send-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: var(--human-message-background);
|
||||
color: var(--human-message-text-color);
|
||||
border: 2px solid var(--human-message-text-color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background-color: var(--human-message-background);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading spinner for send button */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Flexbox layout for send button mode */
|
||||
.form-actions.with-send-button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -24,6 +24,7 @@
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
@keydown.enter="handleEnterKey"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
@@ -37,6 +38,7 @@
|
||||
:step="stepValue"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
@keydown.enter="handleEnterKey"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
@@ -49,6 +51,7 @@
|
||||
:rows="field.rows || 3"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
@keydown="handleTextareaKeydown"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box; resize: vertical;"
|
||||
></textarea>
|
||||
|
||||
@@ -196,7 +199,7 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal'],
|
||||
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal', 'keydown-enter'],
|
||||
setup() {
|
||||
// Consent text constants (English base)
|
||||
const consentTexts = {
|
||||
@@ -321,6 +324,25 @@ export default {
|
||||
openTermsModal(event) {
|
||||
event.preventDefault();
|
||||
this.$emit('open-terms-modal');
|
||||
},
|
||||
|
||||
// Handle Enter key press for text and number inputs
|
||||
handleEnterKey(event) {
|
||||
console.log('FormField: Enter pressed in field:', this.fieldId);
|
||||
event.preventDefault();
|
||||
this.$emit('keydown-enter');
|
||||
},
|
||||
|
||||
// Handle keydown for textarea (Enter to submit, Shift+Enter for line breaks)
|
||||
handleTextareaKeydown(event) {
|
||||
console.log('FormField: Textarea keydown in field:', this.fieldId, 'Key:', event.key, 'Ctrl:', event.ctrlKey, 'Shift:', event.shiftKey);
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
// Plain Enter submits the form
|
||||
console.log('FormField: Textarea Enter triggered for field:', this.fieldId);
|
||||
event.preventDefault();
|
||||
this.$emit('keydown-enter');
|
||||
}
|
||||
// Shift+Enter allows line breaks in textarea
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -462,7 +484,6 @@ export default {
|
||||
.field-context {
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
|
||||
@@ -7,32 +7,35 @@
|
||||
<slot name="loading"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="normalMessages.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>
|
||||
|
||||
<!-- Normal message list (excluding temporarily positioned AI messages) -->
|
||||
<template v-if="normalMessages.length > 0">
|
||||
<!-- Messages -->
|
||||
<template v-for="(message, index) in normalMessages" :key="message.id">
|
||||
<!-- The actual message -->
|
||||
<chat-message
|
||||
:message="message"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:is-latest-ai-message="isLatestAiMessage(message)"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
></chat-message>
|
||||
<!-- Messages wrapper for bottom alignment -->
|
||||
<div class="messages-wrapper">
|
||||
<!-- Empty state (only show when no messages) -->
|
||||
<div v-if="normalMessages.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>
|
||||
|
||||
<!-- Normal message list (excluding temporarily positioned AI messages) -->
|
||||
<template v-if="normalMessages.length > 0">
|
||||
<!-- Messages -->
|
||||
<template v-for="(message, index) in normalMessages" :key="message.id">
|
||||
<!-- The actual message -->
|
||||
<chat-message
|
||||
:message="message"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:is-latest-ai-message="isLatestAiMessage(message)"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
></chat-message>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<typing-indicator v-if="isTyping"></typing-indicator>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<typing-indicator v-if="isTyping"></typing-indicator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -96,14 +99,20 @@ export default {
|
||||
watch: {
|
||||
messages: {
|
||||
handler(newMessages, oldMessages) {
|
||||
// Auto-scroll when new messages are added
|
||||
if (this.autoScroll && newMessages.length > (oldMessages?.length || 0)) {
|
||||
const hasNewMessages = newMessages.length > (oldMessages?.length || 0);
|
||||
|
||||
// Always auto-scroll when new messages are added (regardless of current scroll position)
|
||||
if (this.autoScroll && hasNewMessages) {
|
||||
// Double $nextTick for better DOM update synchronization
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
deep: true,
|
||||
immediate: false
|
||||
},
|
||||
isTyping(newVal) {
|
||||
if (newVal && this.autoScroll) {
|
||||
@@ -188,13 +197,16 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
scrollToBottom(force = false) {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.isAtBottom = true;
|
||||
this.showScrollButton = false;
|
||||
this.unreadCount = 0;
|
||||
// Use requestAnimationFrame for better timing
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.isAtBottom = true;
|
||||
this.showScrollButton = false;
|
||||
this.unreadCount = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -209,7 +221,7 @@ export default {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (!container) return;
|
||||
|
||||
const threshold = 100; // pixels from bottom
|
||||
const threshold = 50; // Reduced threshold for better detection
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||
|
||||
this.isAtBottom = isNearBottom;
|
||||
@@ -221,7 +233,7 @@ export default {
|
||||
},
|
||||
|
||||
handleImageLoaded() {
|
||||
// Auto-scroll when images load to maintain position
|
||||
// Auto-scroll when img load to maintain position
|
||||
if (this.isAtBottom) {
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
}
|
||||
@@ -273,8 +285,19 @@ export default {
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
/* Bottom-aligned messages implementation */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.messages-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px; /* Space between messages */
|
||||
}
|
||||
|
||||
.load-more-indicator {
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user