- Invoer van een 'constanten' cache op niveau van useTranslation.js, om in de ProgressTracker de boodschappen in de juiste taal te zetten.
484 lines
16 KiB
Vue
484 lines
16 KiB
Vue
<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="hasMeaningfulFormValues(message)" 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>
|
|
<input
|
|
v-else
|
|
type="text"
|
|
:placeholder="field.placeholder || ''"
|
|
class="form-input"
|
|
>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Bericht tekst -->
|
|
<div v-if="message.content" class="message-text" v-html="formatMessage(message.content)"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Error messages -->
|
|
<template v-else-if="message.type === 'error'">
|
|
<div class="message-content error-content">
|
|
<div class="form-error">
|
|
{{ message.content }}
|
|
</div>
|
|
<button v-if="message.retryable" @click="$emit('retry-message', message.id)" class="retry-btn">
|
|
Opnieuw proberen
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Other message types -->
|
|
<template v-else>
|
|
<div class="message-content">
|
|
<div class="message-text" v-html="formatMessage(message.content)"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// Import benodigde componenten
|
|
import DynamicForm from './DynamicForm.vue';
|
|
import ProgressTracker from './ProgressTracker.vue';
|
|
import { useIconManager } from '../js/composables/useIconManager.js';
|
|
|
|
export default {
|
|
name: 'ChatMessage',
|
|
components: {
|
|
'dynamic-form': DynamicForm,
|
|
'progress-tracker': ProgressTracker
|
|
},
|
|
setup(props) {
|
|
const { watchIcon } = useIconManager();
|
|
|
|
// Watch message.formData.icon for automatic icon loading
|
|
watchIcon(() => props.message.formData?.icon);
|
|
|
|
return {};
|
|
},
|
|
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: ''
|
|
}
|
|
},
|
|
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
|
|
data() {
|
|
return {
|
|
formVisible: true
|
|
};
|
|
},
|
|
created() {
|
|
// Icon loading is now handled automatically by useIconManager composable
|
|
|
|
// 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;
|
|
}
|
|
},
|
|
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: {
|
|
hasMeaningfulFormValues(message) {
|
|
// Check if message is user message and has formValues
|
|
if (!message || message.sender !== 'user' || !message.formValues) {
|
|
return false;
|
|
}
|
|
|
|
// Check if formData exists and has fields
|
|
if (!message.formData || !message.formData.fields || Object.keys(message.formData.fields).length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if formValues object exists and isn't empty
|
|
if (!message.formValues || Object.keys(message.formValues).length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if any formValues have actual meaningful content
|
|
const hasActualValues = Object.entries(message.formValues).some(([key, value]) => {
|
|
// Skip if the field doesn't exist in formData
|
|
if (!message.formData.fields[key]) return false;
|
|
|
|
// Check for meaningful values
|
|
if (value === null || value === undefined) return false;
|
|
if (typeof value === 'string' && value.trim() === '') return false;
|
|
if (typeof value === 'boolean') return true; // Boolean values are always meaningful
|
|
if (Array.isArray(value) && value.length === 0) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
return hasActualValues;
|
|
},
|
|
|
|
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;
|
|
this.message.originalContent = 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}`;
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 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;
|
|
}
|
|
|
|
.error-content {
|
|
background-color: #ffebee;
|
|
border: 1px solid #f44336;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.retry-btn {
|
|
margin-top: 10px;
|
|
padding: 8px 16px;
|
|
background-color: #f44336;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.retry-btn:hover {
|
|
background-color: #d32f2f;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 768px) {
|
|
.message {
|
|
max-width: 95%;
|
|
}
|
|
|
|
.form-result-table td:first-child {
|
|
width: 40%;
|
|
}
|
|
}
|
|
</style> |