Files
eveAI/eveai_chat_client/static/assets/vue-components/ChatInput.vue
Josako b6512b2d8c - Aanpassing layout van de chat-input. Character counter is ook weg op desktop. Scrollbar enkel zichtbaar indien nodig. Meer beschikbare ruimte in mobiele client. kleinere radius in de hoeken.
- Gewijzigde logica voor hoogtebepaling chat-input en message history, zodat ook de mobiele client correct functioneert.
2025-09-22 16:54:39 +02:00

559 lines
18 KiB
Vue

<template>
<div class="chat-input-container">
<!-- Material Icons worden nu globaal geladen in scripts.html -->
<!-- Active AI Message Area -->
<div v-if="activeAiMessage" class="active-ai-message-area">
<chat-message
:message="activeAiMessage"
:is-submitting-form="false"
:api-prefix="apiPrefix"
:is-latest-ai-message="true"
:is-in-input-area="true"
@image-loaded="handleImageLoaded"
@specialist-complete="$emit('specialist-complete', $event)"
@specialist-error="$emit('specialist-error', $event)"
></chat-message>
</div>
<!-- Dynamisch formulier container -->
<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
v-if="formData && formData.fields"
:form-data="formData"
:form-values="formValues"
:api-prefix="apiPrefix"
:is-submitting="isLoading"
: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>
</div>
<div v-if="!formData" class="chat-input">
<!-- Main input area -->
<div class="input-main">
<textarea
ref="messageInput"
v-model="localMessage"
@keydown="handleKeydown"
@focus="autoResize"
:placeholder="translatedPlaceholder"
rows="1"
:disabled="isLoading"
:maxlength="maxLength"
class="message-input"
:class="{ 'over-limit': isOverLimit }"
></textarea>
</div>
<!-- Input actions -->
<div class="input-actions">
<!-- Message send button -->
<button
@click="sendMessage"
class="send-btn"
:disabled="!canSend"
: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">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
</div>
</template>
<script>
// Importeer de benodigde componenten
import DynamicForm from './DynamicForm.vue';
import ChatMessage from './ChatMessage.vue';
import { useIconManager } from '../js/composables/useIconManager.js';
import { useTranslationClient } from '../js/composables/useTranslation.js';
export default {
name: 'ChatInput',
components: {
'dynamic-form': DynamicForm,
'chat-message': ChatMessage
},
setup(props) {
const { watchIcon } = useIconManager();
const { translateSafe, isTranslating: isTranslatingText } = useTranslationClient();
// Watch formData.icon for automatic icon loading
watchIcon(() => props.formData?.icon);
return {
translateSafe,
isTranslatingText
};
},
props: {
currentMessage: {
type: String,
default: ''
},
isLoading: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: 'Typ je bericht hier... - Enter om te verzenden, Shift+Enter voor nieuwe regel'
},
maxLength: {
type: Number,
default: 2000
},
formData: {
type: Object,
default: null
},
activeAiMessage: {
type: Object,
default: null
},
apiPrefix: {
type: String,
default: ''
}
},
emits: ['send-message', 'update-message', 'submit-form', 'specialist-complete', 'specialist-error'],
data() {
return {
localMessage: this.currentMessage,
formValues: {},
translatedPlaceholder: this.placeholder,
isTranslating: false,
languageChangeHandler: null
};
},
computed: {
isOverLimit() {
return this.localMessage.length > this.maxLength;
},
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));
},
canSend() {
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() {
return this.formData && this.validateForm();
},
sendButtonText() {
if (this.isLoading) {
return 'Verzenden...';
}
return this.formData ? 'Verstuur formulier' : 'Verstuur bericht';
}
},
watch: {
formData: {
handler(newFormData, oldFormData) {
console.log('ChatInput formData changed:', newFormData);
if (!newFormData) {
console.log('FormData is null of undefined');
this.formValues = {};
return;
}
// Controleer of velden aanwezig zijn
if (!newFormData.fields) {
console.error('FormData bevat geen velden!', newFormData);
return;
}
console.log('Velden in formData:', newFormData.fields);
console.log('Aantal velden:', Array.isArray(newFormData.fields)
? newFormData.fields.length
: Object.keys(newFormData.fields).length);
// Initialiseer formulierwaarden
this.initFormValues();
// Log de geïnitialiseerde waarden
console.log('Formulierwaarden geïnitialiseerd:', this.formValues);
},
immediate: true,
deep: true
},
currentMessage(newVal) {
this.localMessage = newVal;
},
localMessage(newVal) {
this.$emit('update-message', newVal);
this.autoResize();
}
},
created() {
// Als er een formData.icon is, wordt deze automatisch geladen via useIconManager composable
// Geen expliciete window.iconManager calls meer nodig
// Maak een benoemde handler voor betere cleanup
this.languageChangeHandler = (event) => {
if (event.detail && event.detail.language) {
this.handleLanguageChange(event);
}
};
// Luister naar taalwijzigingen
document.addEventListener('language-changed', this.languageChangeHandler);
},
mounted() {
this.autoResize();
// Debug informatie over formData bij initialisatie
console.log('ChatInput mounted, formData:', this.formData);
if (this.formData) {
console.log('FormData bij mount:', JSON.stringify(this.formData));
}
// Herbereken bij viewport-wijziging (bv. rotatie op mobiel)
window.addEventListener('resize', this.autoResize, { passive: true });
},
beforeUnmount() {
// Verwijder event listener bij unmount met de benoemde handler
if (this.languageChangeHandler) {
document.removeEventListener('language-changed', this.languageChangeHandler);
}
window.removeEventListener('resize', this.autoResize);
},
methods: {
handleLanguageChange(event) {
if (event.detail && event.detail.language) {
this.translatePlaceholder(event.detail.language);
}
},
async translatePlaceholder(language) {
// Voorkom dubbele vertaling
if (this.isTranslating) {
console.log('Placeholder vertaling al bezig, overslaan...');
return;
}
// Zet de vertaling vlag
this.isTranslating = true;
// Gebruik de originele placeholder als basis voor vertaling
const originalText = this.placeholder;
try {
// Gebruik moderne translateSafe composable
const apiPrefix = window.chatConfig?.apiPrefix || '';
const translatedText = await this.translateSafe(originalText, language, {
context: 'chat_input_placeholder',
apiPrefix,
fallbackText: originalText
});
// Update de placeholder
this.translatedPlaceholder = translatedText;
console.log('Placeholder succesvol vertaald naar:', language);
} catch (error) {
console.error('Fout bij vertalen placeholder:', error);
// Fallback naar originele tekst
this.translatedPlaceholder = originalText;
} finally {
// Reset de vertaling vlag
this.isTranslating = false;
// Herbereken hoogte na vertaling
this.$nextTick(this.autoResize);
}
},
initFormValues() {
if (this.formData && this.formData.fields) {
console.log('Initializing form values for fields:', this.formData.fields);
this.formValues = {};
// Verwerk array van velden
if (Array.isArray(this.formData.fields)) {
this.formData.fields.forEach(field => {
const fieldId = field.id || field.name;
if (fieldId) {
this.formValues[fieldId] = field.default !== undefined ? field.default : '';
}
});
}
// Verwerk object van velden
else if (typeof this.formData.fields === 'object') {
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
this.formValues[fieldId] = field.default !== undefined ? field.default : '';
});
}
console.log('Initialized form values:', this.formValues);
}
},
handleKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.sendMessage();
} else if (event.key === 'Escape') {
this.localMessage = '';
}
},
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;
},
// 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
},
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;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
});
},
focusInput() {
this.$refs.messageInput?.focus();
},
clearInput() {
this.localMessage = '';
this.focusInput();
},
updateFormValues(newValues) {
// Controleer of er daadwerkelijk iets is veranderd om recursieve updates te voorkomen
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
this.formValues = JSON.parse(JSON.stringify(newValues));
}
},
handleImageLoaded() {
// Handle image loaded events from ChatMessage component
// This method can be used for layout adjustments if needed
console.log('Image loaded in active AI message');
}
}
};
</script>
<style scoped>
/* ChatInput component styling */
/* Algemene container */
.chat-input-container {
width: 100%;
max-width: 1000px;
padding: 20px;
box-sizing: border-box;
background-color: var(--active-background-color);
color: var(--human-message-text-color);
border-top: 1px solid #e0e0e0;
font-family: Arial, sans-serif;
font-size: 14px;
transition: opacity 0.2s ease-in-out;
margin-left: auto;
margin-right: auto;
}
/* Input veld en knoppen */
.chat-input {
display: flex;
align-items: flex-end;
gap: 10px;
}
.input-main {
flex: 1;
position: relative;
/* Zorg ervoor dat er ruimte is voor de verzendknop */
margin-right: 0;
}
.message-input {
width: 100%;
min-height: 40px;
padding: 10px 15px 10px 15px; /* counter verwijderd -> rechter padding omlaag */
border: 1px solid #ddd;
border-radius: 10px;
resize: none;
outline: none;
transition: border-color 0.2s;
font-family: Arial, sans-serif;
font-size: 14px;
/* Transparante achtergrond in plaats van wit */
background-color: var(--human-message-background);
color: var(--human-message-text-color);
/* Box-sizing om padding correct te berekenen */
box-sizing: border-box;
/* Laat intern scrollen toe bij >120px, maar verberg scrollbar visueel */
overflow: auto;
-webkit-overflow-scrolling: touch; /* soepel scrollen op iOS */
scrollbar-width: none; /* Firefox: verberg scrollbar */
}
/* WebKit/Chromium: scrollbar verbergen */
.message-input::-webkit-scrollbar {
display: none;
}
/* Input actions */
.input-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; /* Voorkom dat de knop krimpt */
}
/* Verzendknop */
.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;
flex-shrink: 0; /* Voorkom dat de knop krimpt */
}
.send-btn:hover:not(:disabled) {
background-color: var(--human-message-background);
}
.send-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* Loading spinner */
.loading-spinner {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Active AI Message Area - positioned at top of ChatInput */
.active-ai-message-area {
margin-bottom: 15px;
padding: 12px;
background-color: var(--ai-message-background);
color: var(--ai-message-text-color);
border-radius: 8px;
font-family: Arial, sans-serif;
font-size: 14px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
}
/* Ensure the active AI message integrates well with ChatInput styling */
.active-ai-message-area .message {
margin-bottom: 0;
max-width: 100%;
}
.active-ai-message-area .message-content {
background-color: transparent;
border: none;
padding: 0;
}
</style>