- 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:
Josako
2025-08-02 16:36:41 +02:00
parent 998ddf4c03
commit 9a88582fff
50 changed files with 2064 additions and 384 deletions

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;