Chat client changes
- Form values shown correct in MessageHistory of Chat client - Improements to CSS - Move css en js to assets directory - Introduce better Personal Contact Form & Professional Contact Form - Start working on actual Selection Specialist
This commit is contained in:
318
eveai_chat_client/static/assets/js/components/ChatMessage.js
Normal file
318
eveai_chat_client/static/assets/js/components/ChatMessage.js
Normal file
@@ -0,0 +1,318 @@
|
||||
// Voeg stylesheets toe voor formulier en chat berichten weergave
|
||||
const addStylesheets = () => {
|
||||
// Formulier stylesheet
|
||||
if (!document.querySelector('link[href*="form-message.css"]')) {
|
||||
const formLink = document.createElement('link');
|
||||
formLink.rel = 'stylesheet';
|
||||
formLink.href = '/static/assets/css/form-message.css';
|
||||
document.head.appendChild(formLink);
|
||||
}
|
||||
|
||||
// Chat bericht stylesheet
|
||||
if (!document.querySelector('link[href*="chat-message.css"]')) {
|
||||
const chatLink = document.createElement('link');
|
||||
chatLink.rel = 'stylesheet';
|
||||
chatLink.href = '/static/assets/css/chat-message.css';
|
||||
document.head.appendChild(chatLink);
|
||||
}
|
||||
|
||||
// Material Icons font stylesheet
|
||||
if (!document.querySelector('link[href*="Material+Symbols+Outlined"]')) {
|
||||
const iconLink = document.createElement('link');
|
||||
iconLink.rel = 'stylesheet';
|
||||
iconLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0';
|
||||
document.head.appendChild(iconLink);
|
||||
}
|
||||
};
|
||||
|
||||
// Laad de stylesheets
|
||||
addStylesheets();
|
||||
|
||||
export const ChatMessage = {
|
||||
name: 'ChatMessage',
|
||||
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);
|
||||
}
|
||||
},
|
||||
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
|
||||
};
|
||||
},
|
||||
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: {
|
||||
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>
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user