- introductie van vue files - bijna werkende versie van eveai_chat_client.
This commit is contained in:
454
eveai_chat_client/static/assets/vue-components/ChatMessage.vue
Normal file
454
eveai_chat_client/static/assets/vue-components/ChatMessage.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<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>
|
||||
<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';
|
||||
|
||||
export default {
|
||||
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: ''
|
||||
}
|
||||
},
|
||||
emits: ['image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
formVisible: true
|
||||
};
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'message.formData.icon': {
|
||||
handler(newIcon) {
|
||||
if (newIcon && window.iconManager) {
|
||||
window.iconManager.loadIcon(newIcon);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</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>
|
||||
Reference in New Issue
Block a user