318 lines
14 KiB
JavaScript
318 lines
14 KiB
JavaScript
// Import benodigde componenten
|
||
import { DynamicForm } from './DynamicForm.js';
|
||
import { ProgressTracker } from './ProgressTracker.js';
|
||
|
||
// CSS en Material Icons worden nu geladen via de hoofdbundel en scripts.html
|
||
// We hoeven hier niets dynamisch te laden
|
||
|
||
export const ChatMessage = {
|
||
name: 'ChatMessage',
|
||
components: {
|
||
'dynamic-form': DynamicForm,
|
||
'progress-tracker': ProgressTracker
|
||
},
|
||
props: {
|
||
message: {
|
||
type: Object,
|
||
required: true,
|
||
validator: (message) => {
|
||
return message.id && message.content !== undefined && message.sender && message.type;
|
||
}
|
||
},
|
||
isSubmittingForm: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
apiPrefix: {
|
||
type: String,
|
||
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);
|
||
}
|
||
|
||
// Sla de originele inhoud op voor het eerste bericht als we in een conversatie zitten met slechts één bericht
|
||
if (this.message.sender === 'ai' && !this.message.originalContent) {
|
||
this.message.originalContent = this.message.content;
|
||
}
|
||
},
|
||
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
|
||
};
|
||
},
|
||
mounted() {
|
||
// Luister naar taalwijzigingen
|
||
document.addEventListener('language-changed', this.handleLanguageChange);
|
||
},
|
||
beforeUnmount() {
|
||
// Verwijder event listener bij verwijderen component
|
||
document.removeEventListener('language-changed', this.handleLanguageChange);
|
||
},
|
||
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: {
|
||
async handleLanguageChange(event) {
|
||
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
||
// Dit wordt al afgehandeld door MessageHistory component, dus we hoeven hier niets te doen
|
||
// De implementatie hiervan blijft in MessageHistory om dubbele vertaling te voorkomen
|
||
},
|
||
|
||
handleSpecialistError(eventData) {
|
||
console.log('ChatMessage received specialist-error event:', eventData);
|
||
|
||
// Creëer een error message met correcte styling
|
||
this.message.type = 'error';
|
||
this.message.content = eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.';
|
||
this.message.retryable = true;
|
||
this.message.error = true; // Voeg error flag toe voor styling
|
||
|
||
// Bubble up naar parent component voor verdere afhandeling
|
||
this.$emit('specialist-error', {
|
||
messageId: this.message.id,
|
||
...eventData
|
||
});
|
||
},
|
||
|
||
handleSpecialistComplete(eventData) {
|
||
console.log('ChatMessage received specialist-complete event:', eventData);
|
||
|
||
// Update de inhoud van het bericht met het antwoord
|
||
if (eventData.answer) {
|
||
console.log('Updating message content with answer:', eventData.answer);
|
||
this.message.content = eventData.answer;
|
||
} else {
|
||
console.error('No answer in specialist-complete event data');
|
||
}
|
||
|
||
// Bubble up naar parent component voor eventuele verdere afhandeling
|
||
this.$emit('specialist-complete', {
|
||
messageId: this.message.id,
|
||
answer: eventData.answer,
|
||
form_request: eventData.form_request, // Wordt nu door ChatApp verwerkt
|
||
result: eventData.result,
|
||
interactionId: eventData.interactionId,
|
||
taskId: eventData.taskId
|
||
});
|
||
},
|
||
|
||
formatMessage(content) {
|
||
if (!content) return '';
|
||
|
||
// Enhanced markdown-like formatting
|
||
return content
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||
.replace(/\n/g, '<br>');
|
||
},
|
||
|
||
|
||
removeMessage() {
|
||
// Dit zou een event moeten triggeren naar de parent component
|
||
},
|
||
|
||
reactToMessage(emoji) {
|
||
// Implementatie van reacties zou hier komen
|
||
},
|
||
|
||
getMessageClass() {
|
||
return `message ${this.message.sender}`;
|
||
}
|
||
},
|
||
template: `
|
||
<div :class="getMessageClass()" :data-message-id="message.id">
|
||
<!-- Normal text messages -->
|
||
<template v-if="message.type === 'text'">
|
||
<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"
|
||
:task-id="message.taskId"
|
||
:api-prefix="apiPrefix"
|
||
class="message-progress"
|
||
@specialist-complete="handleSpecialistComplete"
|
||
@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>
|
||
<!-- Debug info -->
|
||
<div v-if="false" class="debug-info">
|
||
Content: {{ message.content ? message.content.length + ' chars' : 'empty' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Image messages -->
|
||
<template v-if="message.type === 'image'">
|
||
<div class="message-content">
|
||
<img
|
||
:src="message.imageUrl"
|
||
:alt="message.alt || 'Afbeelding'"
|
||
class="message-image"
|
||
@load="$emit('image-loaded')"
|
||
>
|
||
<div v-if="message.caption" class="image-caption">
|
||
{{ message.caption }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- File messages -->
|
||
<template v-if="message.type === 'file'">
|
||
<div class="message-content">
|
||
<div class="file-attachment">
|
||
<div class="file-icon">📎</div>
|
||
<div class="file-info">
|
||
<div class="file-name">{{ message.fileName }}</div>
|
||
<div class="file-size">{{ message.fileSize }}</div>
|
||
</div>
|
||
<a
|
||
:href="message.fileUrl"
|
||
download
|
||
class="file-download"
|
||
title="Download"
|
||
>
|
||
⬇️
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
<!-- System messages -->
|
||
<template v-if="message.type === 'system'">
|
||
<div class="system-message">
|
||
<span class="system-icon">ℹ️</span>
|
||
{{ message.content }}
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Error messages -->
|
||
<template v-if="message.type === 'error'">
|
||
<div class="error-message">
|
||
<span class="error-icon">⚠️</span>
|
||
{{ message.content }}
|
||
<button
|
||
v-if="message.retryable"
|
||
@click="$emit('retry-message', message.id)"
|
||
class="retry-btn"
|
||
>
|
||
Probeer opnieuw
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Message reactions -->
|
||
<div v-if="message.reactions && message.reactions.length" class="message-reactions">
|
||
<span
|
||
v-for="reaction in message.reactions"
|
||
:key="reaction.emoji"
|
||
class="reaction"
|
||
@click="reactToMessage(reaction.emoji)"
|
||
>
|
||
{{ reaction.emoji }} {{ reaction.count }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
`
|
||
}; |