- Min of meer werkende chat client new stule
This commit is contained in:
@@ -76,11 +76,13 @@ def create_app(config_file=None):
|
||||
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
||||
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||
|
||||
# @app.before_request
|
||||
# def app_before_request():
|
||||
# app.logger.debug(f'App before request: {request.path} ===== Method: {request.method} =====')
|
||||
# app.logger.debug(f'Full URL: {request.url}')
|
||||
# app.logger.debug(f'Endpoint: {request.endpoint}')
|
||||
@app.before_request
|
||||
def app_before_request():
|
||||
if request.path.startswith('/healthz'):
|
||||
pass
|
||||
app.logger.debug(f'App before request: {request.path} ===== Method: {request.method} =====')
|
||||
app.logger.debug(f'Full URL: {request.url}')
|
||||
app.logger.debug(f'Endpoint: {request.endpoint}')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,391 +0,0 @@
|
||||
// static/js/components/ChatInput.js
|
||||
|
||||
// Importeer de benodigde componenten
|
||||
import { DynamicForm } from './DynamicForm.js';
|
||||
import { IconManagerMixin } from '../iconManager.js';
|
||||
|
||||
// CSS wordt nu geladen via de main bundle in chat-client.js
|
||||
// We hoeven hier niets dynamisch te laden
|
||||
|
||||
export const ChatInput = {
|
||||
name: 'ChatInput',
|
||||
components: {
|
||||
'dynamic-form': DynamicForm
|
||||
},
|
||||
// Gebruik de IconManagerMixin om automatisch iconen te laden
|
||||
mixins: [IconManagerMixin],
|
||||
created() {
|
||||
// Als er een formData.icon is, wordt deze automatisch geladen via IconManagerMixin
|
||||
// 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);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Verwijder event listener bij unmount met de benoemde handler
|
||||
if (this.languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
},
|
||||
emits: ['send-message', 'update-message', 'submit-form'],
|
||||
watch: {
|
||||
'formData.icon': {
|
||||
handler(newIcon) {
|
||||
if (newIcon && window.iconManager) {
|
||||
window.iconManager.ensureIconsLoaded({}, [newIcon]);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localMessage: this.currentMessage,
|
||||
formValues: {},
|
||||
translatedPlaceholder: this.placeholder,
|
||||
isTranslating: false,
|
||||
languageChangeHandler: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
characterCount() {
|
||||
return this.localMessage.length;
|
||||
},
|
||||
|
||||
isOverLimit() {
|
||||
return this.characterCount > 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() {
|
||||
const hasValidForm = this.formData && this.validateForm();
|
||||
const hasValidMessage = this.localMessage.trim() && !this.isOverLimit;
|
||||
|
||||
// We kunnen nu verzenden als er een geldig formulier OF een geldig bericht is
|
||||
// Bij een formulier is aanvullende tekst optioneel
|
||||
return (!this.isLoading) && (hasValidForm || hasValidMessage);
|
||||
},
|
||||
|
||||
hasFormDataToSend() {
|
||||
return this.formData && this.validateForm();
|
||||
},
|
||||
|
||||
sendButtonText() {
|
||||
if (this.isLoading) {
|
||||
return 'Verzenden...';
|
||||
}
|
||||
return this.formData ? 'Verstuur formulier' : 'Verstuur bericht';
|
||||
}
|
||||
},
|
||||
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));
|
||||
}
|
||||
},
|
||||
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 {
|
||||
// Controleer of TranslationClient beschikbaar is
|
||||
if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
|
||||
console.error('TranslationClient.translate is niet beschikbaar voor placeholder');
|
||||
return;
|
||||
}
|
||||
|
||||
// Gebruik TranslationClient zonder UI indicator
|
||||
const apiPrefix = window.chatConfig?.apiPrefix || '';
|
||||
const response = await window.TranslationClient.translate(
|
||||
originalText,
|
||||
language,
|
||||
null, // source_lang (auto-detect)
|
||||
'chat_input_placeholder', // context
|
||||
apiPrefix // API prefix voor tenant routing
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
// Update de placeholder
|
||||
this.translatedPlaceholder = response.translated_text;
|
||||
} else {
|
||||
console.error('Vertaling placeholder mislukt:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fout bij vertalen placeholder:', error);
|
||||
} finally {
|
||||
// Reset de vertaling vlag
|
||||
this.isTranslating = false;
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
if (!this.canSend) return;
|
||||
|
||||
// Bij een formulier gaan we het formulier en optioneel bericht combineren
|
||||
if (this.formData) {
|
||||
// Valideer het formulier
|
||||
if (this.validateForm()) {
|
||||
// Verstuur het formulier, eventueel met aanvullende tekst
|
||||
this.$emit('submit-form', this.formValues);
|
||||
}
|
||||
} else if (this.localMessage.trim()) {
|
||||
// Verstuur normaal bericht zonder formulier
|
||||
this.$emit('send-message');
|
||||
}
|
||||
},
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="chat-input-container">
|
||||
<!-- Material Icons worden nu globaal geladen in scripts.html -->
|
||||
<!-- 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"
|
||||
:is-submitting="isLoading"
|
||||
:hide-actions="true"
|
||||
@update:form-values="updateFormValues"
|
||||
></dynamic-form>
|
||||
|
||||
<!-- Geen extra knoppen meer onder het formulier, alles gaat via de hoofdverzendknop -->
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<!-- Main input area -->
|
||||
<div class="input-main">
|
||||
<textarea
|
||||
ref="messageInput"
|
||||
v-model="localMessage"
|
||||
@keydown="handleKeydown"
|
||||
:placeholder="translatedPlaceholder"
|
||||
rows="1"
|
||||
:disabled="isLoading"
|
||||
:maxlength="maxLength"
|
||||
class="message-input"
|
||||
:class="{ 'over-limit': isOverLimit }"
|
||||
></textarea>
|
||||
|
||||
<!-- Character counter -->
|
||||
<div v-if="maxLength" class="character-counter" :class="{ 'over-limit': isOverLimit }">
|
||||
{{ characterCount }}/{{ maxLength }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input actions -->
|
||||
<div class="input-actions">
|
||||
|
||||
<!-- Universele verzendknop (voor zowel berichten als formulieren) -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="send-btn"
|
||||
:class="{ 'form-mode': formData }"
|
||||
:disabled="!canSend"
|
||||
:title="formData ? 'Verstuur formulier' : '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>
|
||||
`
|
||||
};
|
||||
@@ -1,318 +0,0 @@
|
||||
// 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>
|
||||
`
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
export const DynamicForm = {
|
||||
name: 'DynamicForm',
|
||||
created() {
|
||||
// Zorg ervoor dat het icoon geladen wordt als iconManager beschikbaar is
|
||||
if (window.iconManager && this.formData && this.formData.icon) {
|
||||
window.iconManager.loadIcon(this.formData.icon);
|
||||
}
|
||||
},
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (formData) => {
|
||||
// Controleer eerst of formData een geldig object is
|
||||
if (!formData || typeof formData !== 'object') {
|
||||
console.error('FormData is niet een geldig object');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of er een titel of naam is
|
||||
if (!formData.title && !formData.name) {
|
||||
console.error('FormData heeft geen title of name');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of er velden zijn
|
||||
if (!formData.fields) {
|
||||
console.error('FormData heeft geen fields eigenschap');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controleer of velden een array of object zijn
|
||||
if (!Array.isArray(formData.fields) && typeof formData.fields !== 'object') {
|
||||
console.error('FormData.fields is geen array of object');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('FormData is geldig:', formData);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
formValues: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['submit', 'cancel', 'update:formValues'],
|
||||
data() {
|
||||
return {
|
||||
localFormValues: { ...this.formValues }
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
formValues: {
|
||||
handler(newValues) {
|
||||
// Gebruik een vlag om recursieve updates te voorkomen
|
||||
if (JSON.stringify(newValues) !== JSON.stringify(this.localFormValues)) {
|
||||
this.localFormValues = JSON.parse(JSON.stringify(newValues));
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
localFormValues: {
|
||||
handler(newValues) {
|
||||
// Gebruik een vlag om recursieve updates te voorkomen
|
||||
if (JSON.stringify(newValues) !== JSON.stringify(this.formValues)) {
|
||||
this.$emit('update:formValues', JSON.parse(JSON.stringify(newValues)));
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'formData.icon': {
|
||||
handler(newIcon) {
|
||||
if (newIcon && window.iconManager) {
|
||||
window.iconManager.loadIcon(newIcon);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
// Basic validation
|
||||
const missingFields = [];
|
||||
|
||||
if (Array.isArray(this.formData.fields)) {
|
||||
// Valideer array-gebaseerde velden
|
||||
this.formData.fields.forEach(field => {
|
||||
const fieldId = field.id || field.name;
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Valideer object-gebaseerde velden
|
||||
Object.entries(this.formData.fields).forEach(([fieldId, field]) => {
|
||||
if (field.required) {
|
||||
const value = this.localFormValues[fieldId];
|
||||
if (value === undefined || value === null ||
|
||||
(typeof value === 'string' && !value.trim()) ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
const fieldNames = missingFields.join(', ');
|
||||
alert(`De volgende velden zijn verplicht: ${fieldNames}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('submit', this.localFormValues);
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
updateFieldValue(fieldId, value) {
|
||||
this.localFormValues[fieldId] = value;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="dynamic-form" :class="{ 'read-only': readOnly }">
|
||||
<div class="form-header" v-if="formData.title || formData.name || formData.icon" style="margin-bottom: 20px; display: flex; align-items: center;">
|
||||
<div class="form-icon" v-if="formData.icon" style="margin-right: 10px; display: flex; align-items: center;">
|
||||
<span class="material-symbols-outlined" style="font-size: 24px; color: #4285f4;">{{ formData.icon }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-title" style="font-weight: bold; font-size: 1.2em; color: #333;">{{ formData.title || formData.name }}</div>
|
||||
<div v-if="formData.description" class="form-description" style="margin-top: 5px; color: #666; font-size: 0.9em;">{{ formData.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="readOnly" class="form-readonly" style="display: grid; grid-template-columns: 35% 65%; gap: 8px; width: 100%;">
|
||||
<!-- Array-based fields -->
|
||||
<template v-if="Array.isArray(formData.fields)">
|
||||
<template v-for="field in formData.fields" :key="field.id || field.name">
|
||||
<div class="field-label" style="font-weight: 500; color: #555; padding: 4px 0;">{{ field.name }}:</div>
|
||||
<div class="field-value" style="padding: 4px 0;">
|
||||
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
|
||||
{{ localFormValues[field.id || field.name] || field.default || '-' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'options' && (field.allowedValues || field.allowed_values)">
|
||||
{{ localFormValues[field.id || field.name] || field.default || '-' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'boolean'">
|
||||
{{ localFormValues[field.id || field.name] ? 'Ja' : 'Nee' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'text'">
|
||||
<div class="text-value" style="white-space: pre-wrap;">{{ localFormValues[field.id || field.name] || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ localFormValues[field.id || field.name] || field.default || '-' }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<!-- Object-based fields -->
|
||||
<template v-else>
|
||||
<template v-for="(field, fieldId) in formData.fields" :key="fieldId">
|
||||
<div class="field-label" style="font-weight: 500; color: #555; padding: 4px 0;">{{ field.name }}:</div>
|
||||
<div class="field-value" style="padding: 4px 0;">
|
||||
<template v-if="field.type === 'enum' && (field.allowedValues || field.allowed_values)">
|
||||
{{ localFormValues[fieldId] || field.default || '-' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'options' && (field.allowedValues || field.allowed_values)">
|
||||
{{ localFormValues[fieldId] || field.default || '-' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'boolean'">
|
||||
{{ localFormValues[fieldId] ? 'Ja' : 'Nee' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'text'">
|
||||
<div class="text-value" style="white-space: pre-wrap;">{{ localFormValues[fieldId] || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ localFormValues[fieldId] || field.default || '-' }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" novalidate>
|
||||
<div class="form-fields" style="margin-top: 10px;">
|
||||
<template v-if="Array.isArray(formData.fields)">
|
||||
<form-field
|
||||
v-for="field in formData.fields"
|
||||
:key="field.id || field.name"
|
||||
:field-id="field.id || field.name"
|
||||
:field="field"
|
||||
:model-value="localFormValues[field.id || field.name]"
|
||||
@update:model-value="localFormValues[field.id || field.name] = $event"
|
||||
></form-field>
|
||||
</template>
|
||||
<template v-else>
|
||||
<form-field
|
||||
v-for="(field, fieldId) in formData.fields"
|
||||
:key="fieldId"
|
||||
:field-id="fieldId"
|
||||
:field="field"
|
||||
:model-value="localFormValues[fieldId]"
|
||||
@update:model-value="localFormValues[fieldId] = $event"
|
||||
></form-field>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" v-if="!hideActions">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="isSubmitting"
|
||||
:class="{ 'loading': isSubmitting }"
|
||||
>
|
||||
<span v-if="isSubmitting" class="spinner"></span>
|
||||
{{ isSubmitting ? 'Verzenden...' : (formData.submitText || 'Versturen') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="handleCancel"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
{{ formData.cancelText || 'Annuleren' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
export const FormField = {
|
||||
name: 'FormField',
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (field) => {
|
||||
return field.name && field.type;
|
||||
}
|
||||
},
|
||||
fieldId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
value: {
|
||||
get() {
|
||||
// Gebruik default waarde als modelValue undefined is
|
||||
if (this.modelValue === undefined || this.modelValue === null) {
|
||||
if (this.field.type === 'boolean') {
|
||||
return this.field.default === true;
|
||||
}
|
||||
return this.field.default !== undefined ? this.field.default : '';
|
||||
}
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
// Voorkom emit als de waarde niet is veranderd
|
||||
if (JSON.stringify(value) !== JSON.stringify(this.modelValue)) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
fieldType() {
|
||||
// Map Python types naar HTML input types
|
||||
const typeMap = {
|
||||
'str': 'text',
|
||||
'string': 'text',
|
||||
'int': 'number',
|
||||
'integer': 'number',
|
||||
'float': 'number',
|
||||
'text': 'textarea',
|
||||
'enum': 'select',
|
||||
'options': 'radio',
|
||||
'boolean': 'checkbox'
|
||||
};
|
||||
return typeMap[this.field.type] || this.field.type;
|
||||
},
|
||||
stepValue() {
|
||||
return this.field.type === 'float' ? 'any' : 1;
|
||||
},
|
||||
description() {
|
||||
return this.field.description || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.value = file;
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="form-field" style="margin-bottom: 15px; display: grid; grid-template-columns: 35% 65%; align-items: center;">
|
||||
<!-- Label voor alle velden behalve boolean/checkbox die een speciale behandeling krijgen -->
|
||||
<label v-if="fieldType !== 'checkbox'" :for="fieldId" style="margin-right: 10px; font-weight: 500;">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Container voor input velden -->
|
||||
<div style="width: 100%;">
|
||||
<!-- Context informatie indien aanwezig -->
|
||||
<div v-if="field.context" class="field-context" style="margin-bottom: 8px; font-size: 0.9em; color: #666; background-color: #f8f9fa; padding: 8px; border-radius: 4px; border-left: 3px solid #4285f4;">
|
||||
{{ field.context }}
|
||||
</div>
|
||||
<!-- Tekstinvoer (string/str) -->
|
||||
<input
|
||||
v-if="fieldType === 'text'"
|
||||
:id="fieldId"
|
||||
type="text"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Numerieke invoer (int/float) -->
|
||||
<input
|
||||
v-if="fieldType === 'number'"
|
||||
:id="fieldId"
|
||||
type="number"
|
||||
v-model.number="value"
|
||||
:required="field.required"
|
||||
:step="stepValue"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; box-sizing: border-box;"
|
||||
>
|
||||
|
||||
<!-- Tekstvlak (text) -->
|
||||
<textarea
|
||||
v-if="fieldType === 'textarea'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:rows="field.rows || 3"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; resize: vertical; box-sizing: border-box;"
|
||||
></textarea>
|
||||
|
||||
<!-- Dropdown (enum) -->
|
||||
<select
|
||||
v-if="fieldType === 'select'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd; background-color: white; box-sizing: border-box;"
|
||||
>
|
||||
<option v-if="!field.required" value="">Selecteer een optie</option>
|
||||
<option
|
||||
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- Debug info voor select field -->
|
||||
<div v-if="fieldType === 'select' && (!(field.allowedValues || field.allowed_values) || (field.allowedValues || field.allowed_values).length === 0)"
|
||||
style="color: #d93025; font-size: 0.85em; margin-top: 4px;">
|
||||
Geen opties beschikbaar voor dit veld.
|
||||
</div>
|
||||
|
||||
<!-- Radio buttons (options) -->
|
||||
<div v-if="fieldType === 'radio'" class="radio-options">
|
||||
<div v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
class="radio-option"
|
||||
style="margin-bottom: 8px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input
|
||||
type="radio"
|
||||
:id="fieldId + '_' + option"
|
||||
:name="fieldId"
|
||||
:value="option"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
<label :for="fieldId + '_' + option" style="cursor: pointer; margin-bottom: 0;">
|
||||
{{ option }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Debug info voor radio options -->
|
||||
<div v-if="!(field.allowedValues || field.allowed_values) || (field.allowedValues || field.allowed_values).length === 0"
|
||||
style="color: #d93025; font-size: 0.85em; margin-top: 4px;">
|
||||
Geen opties beschikbaar voor dit veld.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle switch voor boolean velden, met speciale layout voor deze velden -->
|
||||
<div v-if="fieldType === 'checkbox'" style="grid-column: 1 / span 2;">
|
||||
<!-- Context informatie indien aanwezig -->
|
||||
<div v-if="field.context" class="field-context" style="margin-bottom: 8px; font-size: 0.9em; color: #666; background-color: #f8f9fa; padding: 8px; border-radius: 4px; border-left: 3px solid #4285f4;">
|
||||
{{ field.context }}
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div class="toggle-switch" style="position: relative; display: inline-block; width: 50px; height: 24px;">
|
||||
<input
|
||||
:id="fieldId"
|
||||
type="checkbox"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
style="opacity: 0; width: 0; height: 0;"
|
||||
>
|
||||
<span
|
||||
class="toggle-slider"
|
||||
style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px;"
|
||||
:style="{ backgroundColor: value ? '#4CAF50' : '#ccc' }"
|
||||
@click="value = !value"
|
||||
>
|
||||
<span
|
||||
class="toggle-knob"
|
||||
style="position: absolute; content: ''; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%;"
|
||||
:style="{ transform: value ? 'translateX(26px)' : 'translateX(0)' }"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
<label :for="fieldId" class="checkbox-label" style="margin-left: 10px; cursor: pointer;">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
|
||||
<span class="checkbox-description" style="display: block; font-size: 0.85em; color: #666;">
|
||||
{{ field.description || '' }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
// static/js/components/FormMessage.js
|
||||
|
||||
export const FormMessage = {
|
||||
name: 'FormMessage',
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
formValues: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasFormData() {
|
||||
return this.formData && this.formData.fields && Object.keys(this.formData.fields).length > 0;
|
||||
},
|
||||
formattedFields() {
|
||||
if (!this.hasFormData) return [];
|
||||
|
||||
return Object.entries(this.formData.fields).map(([fieldId, field]) => {
|
||||
let displayValue = this.formValues[fieldId] || '';
|
||||
|
||||
// Format different field types
|
||||
if (field.type === 'boolean') {
|
||||
displayValue = displayValue ? 'Ja' : 'Nee';
|
||||
} else if (field.type === 'enum' && !displayValue && field.default) {
|
||||
displayValue = field.default;
|
||||
} else if (field.type === 'text') {
|
||||
// Voor tekstgebieden, behoud witruimte
|
||||
// De CSS zal dit tonen met white-space: pre-wrap
|
||||
}
|
||||
|
||||
return {
|
||||
id: fieldId,
|
||||
name: field.name,
|
||||
value: displayValue || '-',
|
||||
type: field.type
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="hasFormData" class="form-message">
|
||||
<div v-if="formData.name" class="form-message-header">
|
||||
<i v-if="formData.icon" class="material-icons form-message-icon">{{ formData.icon }}</i>
|
||||
<span class="form-message-title">{{ formData.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-message-fields">
|
||||
<div v-for="field in formattedFields" :key="field.id" class="form-message-field">
|
||||
<div class="field-message-label">{{ field.name }}:</div>
|
||||
<div class="field-message-value" :class="{'text-value': field.type === 'text'}">{{ field.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
export const LanguageSelector = {
|
||||
name: 'LanguageSelector',
|
||||
props: {
|
||||
initialLanguage: {
|
||||
type: String,
|
||||
default: 'nl'
|
||||
},
|
||||
currentLanguage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
supportedLanguageDetails: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
allowedLanguages: {
|
||||
type: Array,
|
||||
default: () => ['nl', 'en', 'fr', 'de']
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const startLanguage = this.currentLanguage || this.initialLanguage;
|
||||
return {
|
||||
selectedLanguage: startLanguage,
|
||||
internalCurrentLanguage: startLanguage
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log('🔍 [DEBUG] LanguageSelector mounted with Vue template');
|
||||
console.log('🔍 [DEBUG] Props:', {
|
||||
initialLanguage: this.initialLanguage,
|
||||
currentLanguage: this.currentLanguage,
|
||||
supportedLanguageDetails: this.supportedLanguageDetails,
|
||||
allowedLanguages: this.allowedLanguages
|
||||
});
|
||||
|
||||
// Emit initial language
|
||||
this.$emit('language-changed', this.selectedLanguage);
|
||||
|
||||
// DOM event
|
||||
const event = new CustomEvent('vue:language-changed', {
|
||||
detail: { language: this.selectedLanguage }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
methods: {
|
||||
getAvailableLanguages() {
|
||||
const languages = [];
|
||||
const languagesToUse = (this.allowedLanguages && this.allowedLanguages.length > 0)
|
||||
? this.allowedLanguages
|
||||
: ['nl', 'en', 'fr', 'de'];
|
||||
|
||||
if (this.supportedLanguageDetails && Object.keys(this.supportedLanguageDetails).length > 0) {
|
||||
for (const [langName, langDetails] of Object.entries(this.supportedLanguageDetails)) {
|
||||
const isoCode = langDetails['iso 639-1'];
|
||||
if (languagesToUse.includes(isoCode)) {
|
||||
languages.push({
|
||||
code: isoCode,
|
||||
name: langName,
|
||||
flag: langDetails.flag || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const defaultLanguages = {
|
||||
'nl': { name: 'Nederlands', flag: '🇳🇱' },
|
||||
'en': { name: 'English', flag: '🇬🇧' },
|
||||
'fr': { name: 'Français', flag: '🇫🇷' },
|
||||
'de': { name: 'Deutsch', flag: '🇩🇪' }
|
||||
};
|
||||
|
||||
languagesToUse.forEach(code => {
|
||||
if (defaultLanguages[code]) {
|
||||
languages.push({
|
||||
code: code,
|
||||
name: defaultLanguages[code].name,
|
||||
flag: defaultLanguages[code].flag
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return languages;
|
||||
},
|
||||
|
||||
changeLanguage(languageCode) {
|
||||
console.log(`LanguageSelector: changeLanguage called with ${languageCode}`);
|
||||
|
||||
if (this.internalCurrentLanguage !== languageCode) {
|
||||
this.internalCurrentLanguage = languageCode;
|
||||
this.selectedLanguage = languageCode;
|
||||
|
||||
// Vue component emit
|
||||
this.$emit('language-changed', languageCode);
|
||||
|
||||
// DOM event
|
||||
const event = new CustomEvent('vue:language-changed', {
|
||||
detail: { language: languageCode }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="language-selector">
|
||||
<label for="language-select">Taal / Language:</label>
|
||||
<div class="select-wrapper">
|
||||
<select id="language-select" class="language-select" v-model="selectedLanguage" @change="changeLanguage(selectedLanguage)">
|
||||
<option v-for="lang in getAvailableLanguages()" :key="lang.code" :value="lang.code">{{ lang.flag }} {{ lang.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
// Import afhankelijke componenten
|
||||
import { ChatMessage } from './ChatMessage.js';
|
||||
import { TypingIndicator } from './TypingIndicator.js';
|
||||
import { ProgressTracker } from './ProgressTracker.js';
|
||||
|
||||
export const MessageHistory = {
|
||||
name: 'MessageHistory',
|
||||
components: {
|
||||
'chat-message': ChatMessage,
|
||||
'typing-indicator': TypingIndicator,
|
||||
'progress-tracker': ProgressTracker
|
||||
},
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
isTyping: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isSubmittingForm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['submit-form', 'load-more', 'specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
isAtBottom: true,
|
||||
unreadCount: 0,
|
||||
originalFirstMessage: null,
|
||||
isTranslating: false, // Vlag om dubbele vertaling te voorkomen
|
||||
languageChangeHandler: null // Referentie voor cleanup
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom();
|
||||
this.setupScrollListener();
|
||||
this.listenForLanguageChanges();
|
||||
|
||||
// Sla de originele inhoud van het eerste bericht op als er maar één bericht is
|
||||
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
|
||||
this.originalFirstMessage = this.messages[0].content;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (this.autoScroll && this.isAtBottom) {
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
listenForLanguageChanges() {
|
||||
// Maak een benoemde handler voor cleanup
|
||||
this.languageChangeHandler = (event) => {
|
||||
if (event.detail && event.detail.language) {
|
||||
this.translateFirstMessageIfNeeded(event.detail.language);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('language-changed', this.languageChangeHandler);
|
||||
},
|
||||
|
||||
async translateFirstMessageIfNeeded(language) {
|
||||
// Voorkom dubbele vertaling
|
||||
if (this.isTranslating) {
|
||||
console.log('Vertaling al bezig, overslaan...');
|
||||
return;
|
||||
}
|
||||
|
||||
// Alleen vertalen als er precies één bericht is en het is van de AI
|
||||
if (this.messages.length === 1 && this.messages[0].sender === 'ai') {
|
||||
const firstMessage = this.messages[0];
|
||||
|
||||
// Controleer of we een origineel bericht hebben om te vertalen
|
||||
const contentToTranslate = this.originalFirstMessage || firstMessage.content;
|
||||
|
||||
// Sla het originele bericht op als we dat nog niet hebben gedaan
|
||||
if (!this.originalFirstMessage) {
|
||||
this.originalFirstMessage = contentToTranslate;
|
||||
}
|
||||
|
||||
// Zet de vertaling vlag
|
||||
this.isTranslating = true;
|
||||
|
||||
try {
|
||||
// Controleer of de vertaalclient beschikbaar is
|
||||
if (!window.TranslationClient || typeof window.TranslationClient.translate !== 'function') {
|
||||
console.error('TranslationClient.translate is niet beschikbaar');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Vertalen van eerste bericht naar ${language}`);
|
||||
|
||||
// Vertaal het bericht met de juiste context
|
||||
const response = await window.TranslationClient.translate(
|
||||
contentToTranslate,
|
||||
language,
|
||||
null, // source_lang (auto-detect)
|
||||
'first_message', // context
|
||||
this.apiPrefix // API prefix voor tenant routing
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
console.log('Vertaling van eerste bericht voltooid:', response.translated_text);
|
||||
// Update het bericht zonder een indicator te tonen
|
||||
firstMessage.content = response.translated_text;
|
||||
} else {
|
||||
console.error('Vertaling van eerste bericht mislukt:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fout bij het vertalen van eerste bericht:', error);
|
||||
} finally {
|
||||
// Reset de vertaling vlag
|
||||
this.isTranslating = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.isAtBottom = true;
|
||||
this.showScrollButton = false;
|
||||
this.unreadCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
setupScrollListener() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (!container) return;
|
||||
|
||||
container.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (!container) return;
|
||||
|
||||
const threshold = 100; // pixels from bottom
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||
|
||||
this.isAtBottom = isNearBottom;
|
||||
|
||||
// Load more messages when scrolled to top
|
||||
if (container.scrollTop === 0) {
|
||||
this.$emit('load-more');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
handleImageLoaded() {
|
||||
// Auto-scroll when images load to maintain position
|
||||
if (this.isAtBottom) {
|
||||
this.$nextTick(() => this.scrollToBottom());
|
||||
}
|
||||
},
|
||||
|
||||
searchMessages(query) {
|
||||
// Simple message search
|
||||
if (!query.trim()) return this.messages;
|
||||
|
||||
const searchTerm = query.toLowerCase();
|
||||
return this.messages.filter(message =>
|
||||
message.content &&
|
||||
message.content.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
},
|
||||
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Cleanup scroll listener
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
// Cleanup language change listener
|
||||
if (this.languageChangeHandler) {
|
||||
document.removeEventListener('language-changed', this.languageChangeHandler);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message-history-container">
|
||||
<!-- Messages container -->
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<!-- Loading indicator for load more -->
|
||||
<div v-if="$slots.loading" class="load-more-indicator">
|
||||
<slot name="loading"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="messages.length === 0" class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<div class="empty-text">Nog geen berichten</div>
|
||||
<div class="empty-subtext">Start een gesprek door een bericht te typen!</div>
|
||||
</div>
|
||||
|
||||
<!-- Message list -->
|
||||
<template v-else>
|
||||
<!-- Messages -->
|
||||
<template v-for="(message, index) in messages" :key="message.id">
|
||||
<!-- The actual message -->
|
||||
<chat-message
|
||||
:message="message"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
></chat-message>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<typing-indicator v-if="isTyping"></typing-indicator>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,311 +0,0 @@
|
||||
export const ProgressTracker = {
|
||||
name: 'ProgressTracker',
|
||||
props: {
|
||||
taskId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
apiPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['specialist-complete', 'progress-update', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
progressLines: [],
|
||||
eventSource: null,
|
||||
isCompleted: false,
|
||||
lastLine: '',
|
||||
error: null,
|
||||
connecting: true,
|
||||
finalAnswer: null,
|
||||
hasError: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
progressEndpoint() {
|
||||
return `${this.apiPrefix}/chat/api/task_progress/${this.taskId}`;
|
||||
},
|
||||
displayLines() {
|
||||
return this.isExpanded ? this.progressLines : [
|
||||
this.lastLine || 'Verbinden met taak...'
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connectToEventSource();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.disconnectEventSource();
|
||||
},
|
||||
methods: {
|
||||
connectToEventSource() {
|
||||
try {
|
||||
this.connecting = true;
|
||||
this.error = null;
|
||||
|
||||
// Sluit eventuele bestaande verbinding
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Maak nieuwe SSE verbinding
|
||||
this.eventSource = new EventSource(this.progressEndpoint);
|
||||
|
||||
// Algemene event handler
|
||||
this.eventSource.onmessage = (event) => {
|
||||
this.handleProgressUpdate(event);
|
||||
};
|
||||
|
||||
// Specifieke event handlers per type
|
||||
this.eventSource.addEventListener('progress', (event) => {
|
||||
this.handleProgressUpdate(event, 'progress');
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('EveAI Specialist Complete', (event) => {
|
||||
console.log('Received EveAI Specialist Complete event');
|
||||
this.handleProgressUpdate(event, 'EveAI Specialist Complete');
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('error', (event) => {
|
||||
this.handleError(event);
|
||||
});
|
||||
|
||||
// Status handlers
|
||||
this.eventSource.onopen = () => {
|
||||
this.connecting = false;
|
||||
};
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
console.error('SSE Connection error:', error);
|
||||
this.error = 'Verbindingsfout. Probeer het later opnieuw.';
|
||||
this.connecting = false;
|
||||
|
||||
// Probeer opnieuw te verbinden na 3 seconden
|
||||
setTimeout(() => {
|
||||
if (!this.isCompleted && this.progressLines.length === 0) {
|
||||
this.connectToEventSource();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error setting up event source:', err);
|
||||
this.error = 'Kan geen verbinding maken met de voortgangsupdates.';
|
||||
this.connecting = false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnectEventSource() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleProgressUpdate(event, eventType = null) {
|
||||
try {
|
||||
const update = JSON.parse(event.data);
|
||||
|
||||
// Controleer op verschillende typen updates
|
||||
const processingType = update.processing_type;
|
||||
const data = update.data || {};
|
||||
|
||||
// Process based on processing type
|
||||
let message = this.formatProgressMessage(processingType, data);
|
||||
|
||||
// Alleen bericht toevoegen als er daadwerkelijk een bericht is
|
||||
if (message) {
|
||||
this.progressLines.push(message);
|
||||
this.lastLine = message;
|
||||
}
|
||||
|
||||
// Emit progress update voor parent component
|
||||
this.$emit('progress-update', {
|
||||
processingType,
|
||||
data,
|
||||
message
|
||||
});
|
||||
|
||||
// Handle completion and errors
|
||||
if (processingType === 'EveAI Specialist Complete') {
|
||||
console.log('Processing EveAI Specialist Complete:', data);
|
||||
this.handleSpecialistComplete(data);
|
||||
} else if (processingType === 'EveAI Specialist Error') {
|
||||
this.handleSpecialistError(data);
|
||||
} else if (processingType === 'Task Complete' || processingType === 'Task Error') {
|
||||
this.isCompleted = true;
|
||||
this.disconnectEventSource();
|
||||
}
|
||||
|
||||
// Scroll automatisch naar beneden als uitgevouwen
|
||||
if (this.isExpanded) {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.progressContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing progress update:', err, event.data);
|
||||
}
|
||||
},
|
||||
|
||||
formatProgressMessage(processingType, data) {
|
||||
// Lege data dictionary - toon enkel processing type
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return processingType;
|
||||
}
|
||||
|
||||
// Specifiek bericht als er een message field is
|
||||
if (data.message) {
|
||||
return data.message;
|
||||
}
|
||||
|
||||
// Processing type met name veld als dat bestaat
|
||||
if (data.name) {
|
||||
return `${processingType}: ${data.name}`;
|
||||
}
|
||||
|
||||
// Stap informatie
|
||||
if (data.step) {
|
||||
return `Stap ${data.step}: ${data.description || ''}`;
|
||||
}
|
||||
|
||||
// Voor EveAI Specialist Complete - geen progress message
|
||||
if (processingType === 'EveAI Specialist Complete') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Default: processing type + eventueel data als string
|
||||
return processingType;
|
||||
},
|
||||
|
||||
handleSpecialistComplete(data) {
|
||||
this.isCompleted = true;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Debug logging
|
||||
console.log('Specialist Complete Data:', data);
|
||||
|
||||
// Extract answer from data.result.answer
|
||||
if (data.result) {
|
||||
if (data.result.answer) {
|
||||
this.finalAnswer = data.result.answer;
|
||||
console.log('Final Answer:', this.finalAnswer);
|
||||
|
||||
// Direct update van de parent message als noodoplossing
|
||||
try {
|
||||
if (this.$parent && this.$parent.message) {
|
||||
console.log('Direct update parent message');
|
||||
this.$parent.message.content = data.result.answer;
|
||||
}
|
||||
} catch(err) {
|
||||
console.error('Error updating parent message:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit event to parent met alle relevante data inclusief form_request
|
||||
this.$emit('specialist-complete', {
|
||||
answer: data.result.answer || '',
|
||||
form_request: data.result.form_request, // Voeg form_request toe
|
||||
result: data.result,
|
||||
interactionId: data.interaction_id,
|
||||
taskId: this.taskId
|
||||
});
|
||||
} else {
|
||||
console.error('Missing result.answer in specialist complete data:', data);
|
||||
}
|
||||
},
|
||||
|
||||
handleSpecialistError(data) {
|
||||
this.isCompleted = true;
|
||||
this.hasError = true;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Zet gebruiksvriendelijke foutmelding
|
||||
const errorMessage = "We could not process your request. Please try again later.";
|
||||
this.error = errorMessage;
|
||||
|
||||
// Log de werkelijke fout voor debug doeleinden
|
||||
if (data.Error) {
|
||||
console.error('Specialist Error:', data.Error);
|
||||
}
|
||||
|
||||
// Emit error event naar parent
|
||||
this.$emit('specialist-error', {
|
||||
message: errorMessage,
|
||||
originalError: data.Error,
|
||||
taskId: this.taskId
|
||||
});
|
||||
},
|
||||
|
||||
handleError(event) {
|
||||
console.error('SSE Error event:', event);
|
||||
this.error = 'Er is een fout opgetreden bij het verwerken van updates.';
|
||||
|
||||
// Probeer parse van foutgegevens
|
||||
try {
|
||||
const errorData = JSON.parse(event.data);
|
||||
if (errorData && errorData.message) {
|
||||
this.error = errorData.message;
|
||||
}
|
||||
} catch (err) {
|
||||
// Blijf bij algemene foutmelding als parsing mislukt
|
||||
}
|
||||
},
|
||||
|
||||
toggleExpand() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
|
||||
if (this.isExpanded) {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.progressContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="progress-tracker" :class="{ 'expanded': isExpanded, 'completed': isCompleted && !hasError, 'error': error || hasError }">
|
||||
<div
|
||||
class="progress-header"
|
||||
@click="toggleExpand"
|
||||
:title="isExpanded ? 'Inklappen' : 'Uitklappen voor volledige voortgang'"
|
||||
>
|
||||
<div class="progress-title">
|
||||
<span v-if="connecting" class="spinner"></span>
|
||||
<span v-else-if="error" class="status-icon error">✗</span>
|
||||
<span v-else-if="isCompleted" class="status-icon completed">✓</span>
|
||||
<span v-else class="status-icon in-progress"></span>
|
||||
<span v-if="error">Fout bij verwerking</span>
|
||||
<span v-else-if="isCompleted">Verwerking voltooid</span>
|
||||
<span v-else>Bezig met redeneren...</span>
|
||||
</div>
|
||||
<div class="progress-toggle">
|
||||
{{ isExpanded ? '▲' : '▼' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="progress-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="progressContainer"
|
||||
class="progress-content"
|
||||
:class="{ 'single-line': !isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in displayLines"
|
||||
:key="index"
|
||||
class="progress-line"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
export const TypingIndicator = {
|
||||
name: 'TypingIndicator',
|
||||
props: {
|
||||
showText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: 'Bezig met typen...'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Andere methoden kunnen hier staan
|
||||
},
|
||||
template: `
|
||||
<div class="typing-indicator">
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
<div v-if="showText" class="typing-text">{{ text }}</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
// Component barrel export file
|
||||
// Dit bestand maakt het eenvoudiger om alle componenten in één keer te importeren
|
||||
|
||||
// Importeer eerst alle componenten lokaal
|
||||
import { TypingIndicator } from './TypingIndicator.js';
|
||||
import { FormField } from './FormField.js';
|
||||
import { DynamicForm } from './DynamicForm.js';
|
||||
import { ChatMessage } from './ChatMessage.js';
|
||||
import { MessageHistory } from './MessageHistory.js';
|
||||
import { ProgressTracker } from './ProgressTracker.js';
|
||||
import { LanguageSelector } from './LanguageSelector.js';
|
||||
import { ChatInput } from './ChatInput.js';
|
||||
|
||||
// Exporteer componenten individueel
|
||||
export { TypingIndicator };
|
||||
export { FormField };
|
||||
export { DynamicForm };
|
||||
export { ChatMessage };
|
||||
export { MessageHistory };
|
||||
export { ProgressTracker };
|
||||
export { LanguageSelector };
|
||||
export { ChatInput };
|
||||
|
||||
// Debug logging voor index.js
|
||||
console.log('🔍 [DEBUG] Components index.js geladen, exporteert:', {
|
||||
TypingIndicator: typeof TypingIndicator,
|
||||
ChatMessage: typeof ChatMessage,
|
||||
MessageHistory: typeof MessageHistory,
|
||||
ChatInput: typeof ChatInput
|
||||
});
|
||||
|
||||
// Nu kunnen componenten op verschillende manieren worden geïmporteerd:
|
||||
// 1. import { FormField, ChatMessage } from './components';
|
||||
// 2. import { ChatInput } from './components/ChatInput.js';
|
||||
@@ -27,7 +27,7 @@ const TranslationClient = {
|
||||
if (context) requestData.context = context;
|
||||
|
||||
// Bouw de juiste endpoint URL met prefix
|
||||
const endpoint = `${apiPrefix}/chat/api/translate`;
|
||||
const endpoint = `${apiPrefix}/api/translate`;
|
||||
console.log(`Vertaling aanvragen op endpoint: ${endpoint}`);
|
||||
|
||||
// Doe het API-verzoek
|
||||
|
||||
505
eveai_chat_client/static/assets/vue-components/ChatApp.vue
Normal file
505
eveai_chat_client/static/assets/vue-components/ChatApp.vue
Normal file
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<div class="chat-app-container">
|
||||
<!-- Message History - takes available space -->
|
||||
<message-history
|
||||
:messages="displayMessages"
|
||||
:is-typing="isTyping"
|
||||
:is-submitting-form="isSubmittingForm"
|
||||
:api-prefix="apiPrefix"
|
||||
:auto-scroll="true"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
></message-history>
|
||||
|
||||
<!-- Chat Input - to the bottom -->
|
||||
<chat-input
|
||||
:current-message="currentMessage"
|
||||
:is-loading="isLoading"
|
||||
:max-length="2000"
|
||||
:allow-file-upload="true"
|
||||
:allow-voice-message="false"
|
||||
:form-data="currentInputFormData"
|
||||
@send-message="sendMessage"
|
||||
@update-message="updateCurrentMessage"
|
||||
@upload-file="handleFileUpload"
|
||||
@record-voice="handleVoiceRecord"
|
||||
@submit-form="submitFormFromInput"
|
||||
ref="chatInput"
|
||||
class="chat-input-area"
|
||||
></chat-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import all components as Vue SFCs
|
||||
import TypingIndicator from './TypingIndicator.vue';
|
||||
import FormField from './FormField.vue';
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
import ChatMessage from './ChatMessage.vue';
|
||||
import MessageHistory from './MessageHistory.vue';
|
||||
import ProgressTracker from './ProgressTracker.vue';
|
||||
import LanguageSelector from './LanguageSelector.vue';
|
||||
import ChatInput from './ChatInput.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChatApp',
|
||||
components: {
|
||||
TypingIndicator,
|
||||
FormField,
|
||||
DynamicForm,
|
||||
ChatMessage,
|
||||
MessageHistory,
|
||||
ProgressTracker,
|
||||
ChatInput
|
||||
},
|
||||
|
||||
data() {
|
||||
// Maak een lokale kopie van de chatConfig om undefined errors te voorkomen
|
||||
const chatConfig = window.chatConfig || {};
|
||||
const settings = chatConfig.settings || {};
|
||||
const initialLanguage = chatConfig.language || 'nl';
|
||||
const originalExplanation = chatConfig.explanation || '';
|
||||
const tenantMake = chatConfig.tenantMake || {};
|
||||
|
||||
return {
|
||||
// Tenant info
|
||||
tenantName: tenantMake.name || 'EveAI',
|
||||
tenantLogoUrl: tenantMake.logo_url || '',
|
||||
|
||||
// Taal gerelateerde data
|
||||
currentLanguage: initialLanguage,
|
||||
supportedLanguageDetails: chatConfig.supportedLanguageDetails || {},
|
||||
allowedLanguages: chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
||||
supportedLanguages: chatConfig.supportedLanguages || [],
|
||||
originalExplanation: originalExplanation,
|
||||
explanation: chatConfig.explanation || '',
|
||||
|
||||
// Chat-specific data
|
||||
currentMessage: '',
|
||||
allMessages: [],
|
||||
isTyping: false,
|
||||
isLoading: false,
|
||||
isSubmittingForm: false,
|
||||
messageIdCounter: 1,
|
||||
formValues: {},
|
||||
currentInputFormData: null,
|
||||
|
||||
// API prefix voor endpoints
|
||||
apiPrefix: chatConfig.apiPrefix || '',
|
||||
|
||||
// Configuration from Flask/server
|
||||
conversationId: chatConfig.conversationId || 'default',
|
||||
userId: chatConfig.userId || null,
|
||||
userName: chatConfig.userName || '',
|
||||
|
||||
// Settings met standaard waarden en overschreven door server config
|
||||
settings: {
|
||||
maxMessageLength: settings.maxMessageLength || 2000,
|
||||
allowFileUpload: settings.allowFileUpload === true,
|
||||
allowVoiceMessage: settings.allowVoiceMessage === true,
|
||||
autoScroll: settings.autoScroll === true
|
||||
},
|
||||
|
||||
// UI state
|
||||
isMobile: window.innerWidth <= 768,
|
||||
showSidebar: window.innerWidth > 768,
|
||||
|
||||
// Advanced features
|
||||
messageSearch: '',
|
||||
filteredMessages: [],
|
||||
isSearching: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Keep existing computed from base.html
|
||||
compiledExplanation() {
|
||||
if (typeof marked === 'function') {
|
||||
return marked(this.explanation);
|
||||
} else if (marked && typeof marked.parse === 'function') {
|
||||
return marked.parse(this.explanation.replace(/\[\[(.*?)\]\]/g, '<strong>$1</strong>'));
|
||||
} else {
|
||||
console.error('Marked library not properly loaded');
|
||||
return this.explanation;
|
||||
}
|
||||
},
|
||||
|
||||
displayMessages() {
|
||||
return this.isSearching ? this.filteredMessages : this.allMessages;
|
||||
},
|
||||
|
||||
hasMessages() {
|
||||
return this.allMessages.length > 0;
|
||||
},
|
||||
|
||||
displayLanguages() {
|
||||
// Filter de ondersteunde talen op basis van de toegestane talen
|
||||
if (!this.supportedLanguages || !this.allowedLanguages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.supportedLanguages.filter(lang =>
|
||||
this.allowedLanguages.includes(lang.code)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initializeChat();
|
||||
this.setupEventListeners();
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.cleanup();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Initialization
|
||||
initializeChat() {
|
||||
console.log('Initializing chat application...');
|
||||
|
||||
// Load historical messages from server
|
||||
this.loadHistoricalMessages();
|
||||
|
||||
console.log('Nr of messages:', this.allMessages.length);
|
||||
|
||||
// Add welcome message if no history
|
||||
if (this.allMessages.length === 0) {
|
||||
this.addWelcomeMessage();
|
||||
}
|
||||
|
||||
// Focus input after initialization
|
||||
this.$nextTick(() => {
|
||||
this.focusChatInput();
|
||||
});
|
||||
},
|
||||
|
||||
loadHistoricalMessages() {
|
||||
// Veilige toegang tot messages met fallback
|
||||
const chatConfig = window.chatConfig || {};
|
||||
const historicalMessages = chatConfig.messages || [];
|
||||
|
||||
if (historicalMessages.length > 0) {
|
||||
this.allMessages = historicalMessages
|
||||
.filter(msg => msg !== null && msg !== undefined) // Filter null/undefined berichten uit
|
||||
.map(msg => {
|
||||
// Zorg voor een correct geformatteerde bericht-object
|
||||
return {
|
||||
id: this.messageIdCounter++,
|
||||
content: typeof msg === 'string' ? msg : (msg.content || ''),
|
||||
sender: msg.sender || 'ai',
|
||||
type: msg.type || 'text',
|
||||
timestamp: msg.timestamp || new Date().toISOString(),
|
||||
formData: msg.formData || null,
|
||||
status: msg.status || 'delivered'
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Loaded ${this.allMessages.length} historical messages`);
|
||||
}
|
||||
},
|
||||
|
||||
async addWelcomeMessage() {
|
||||
console.log('Sending initialize message to backend');
|
||||
|
||||
// Toon typing indicator
|
||||
this.isTyping = true;
|
||||
this.isLoading = true;
|
||||
|
||||
console.log('API prefix:', this.apiPrefix);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiPrefix}/api/send_message`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Initialize',
|
||||
language: this.currentLanguage,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Initialize response:', data);
|
||||
|
||||
if (data.task_id) {
|
||||
// Add a placeholder message that will be updated by the progress tracker
|
||||
const placeholderMessage = {
|
||||
id: this.messageIdCounter++,
|
||||
content: 'Bezig met laden...',
|
||||
sender: 'ai',
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId: data.task_id,
|
||||
status: 'processing'
|
||||
};
|
||||
|
||||
this.allMessages.push(placeholderMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending initialize message:', error);
|
||||
this.addMessage({
|
||||
content: 'Er is een fout opgetreden bij het initialiseren van de chat.',
|
||||
sender: 'ai',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
this.isTyping = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Message handling
|
||||
addMessage(messageData) {
|
||||
const message = {
|
||||
id: this.messageIdCounter++,
|
||||
content: messageData.content || '',
|
||||
sender: messageData.sender || 'user',
|
||||
type: messageData.type || 'text',
|
||||
timestamp: messageData.timestamp || new Date().toISOString(),
|
||||
formData: messageData.formData || null,
|
||||
formValues: messageData.formValues || null,
|
||||
status: messageData.status || 'delivered'
|
||||
};
|
||||
|
||||
this.allMessages.push(message);
|
||||
|
||||
// Auto-scroll to bottom
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
if (!this.currentMessage.trim() && !this.currentInputFormData) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Sending message:', this.currentMessage);
|
||||
|
||||
// Add user message to chat
|
||||
const userMessage = this.addMessage({
|
||||
content: this.currentMessage,
|
||||
sender: 'user',
|
||||
formData: this.currentInputFormData,
|
||||
formValues: this.formValues
|
||||
});
|
||||
|
||||
// Clear input
|
||||
const messageToSend = this.currentMessage;
|
||||
const formValuesToSend = { ...this.formValues };
|
||||
this.currentMessage = '';
|
||||
this.formValues = {};
|
||||
this.currentInputFormData = null;
|
||||
|
||||
// Show typing indicator
|
||||
this.isTyping = true;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiPrefix}/api/send_message`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: messageToSend,
|
||||
form_values: Object.keys(formValuesToSend).length > 0 ? formValuesToSend : undefined,
|
||||
language: this.currentLanguage,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Send message response:', data);
|
||||
|
||||
if (data.task_id) {
|
||||
// Add a placeholder AI message that will be updated by the progress tracker
|
||||
const placeholderMessage = {
|
||||
id: this.messageIdCounter++,
|
||||
content: 'Bezig met verwerken...',
|
||||
sender: 'ai',
|
||||
type: 'text',
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId: data.task_id,
|
||||
status: 'processing'
|
||||
};
|
||||
|
||||
this.allMessages.push(placeholderMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
this.addMessage({
|
||||
content: 'Er is een fout opgetreden bij het verzenden van het bericht.',
|
||||
sender: 'ai',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
this.isTyping = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentMessage(message) {
|
||||
this.currentMessage = message;
|
||||
},
|
||||
|
||||
submitFormFromInput(formValues) {
|
||||
console.log('Form submitted from input:', formValues);
|
||||
this.formValues = formValues;
|
||||
this.sendMessage();
|
||||
},
|
||||
|
||||
handleFileUpload(file) {
|
||||
console.log('File upload:', file);
|
||||
// Implement file upload logic
|
||||
},
|
||||
|
||||
handleVoiceRecord(audioData) {
|
||||
console.log('Voice record:', audioData);
|
||||
// Implement voice recording logic
|
||||
},
|
||||
|
||||
// Event handling
|
||||
setupEventListeners() {
|
||||
// Language change listener
|
||||
document.addEventListener('language-changed', (event) => {
|
||||
if (event.detail && event.detail.language) {
|
||||
this.currentLanguage = event.detail.language;
|
||||
console.log(`Language changed to: ${this.currentLanguage}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Window resize listener
|
||||
window.addEventListener('resize', () => {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
this.showSidebar = window.innerWidth > 768;
|
||||
});
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
// Remove event listeners
|
||||
document.removeEventListener('language-changed', this.handleLanguageChange);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
// Specialist event handlers
|
||||
handleSpecialistComplete(eventData) {
|
||||
console.log('Specialist complete event received:', eventData);
|
||||
|
||||
// Find the message with the matching task ID
|
||||
const messageIndex = this.allMessages.findIndex(msg =>
|
||||
msg.taskId === eventData.taskId
|
||||
);
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// Update the message content
|
||||
this.allMessages[messageIndex].content = eventData.answer;
|
||||
this.allMessages[messageIndex].status = 'completed';
|
||||
|
||||
// Handle form request if present
|
||||
if (eventData.form_request) {
|
||||
console.log('Form request received:', eventData.form_request);
|
||||
this.currentInputFormData = eventData.form_request;
|
||||
}
|
||||
}
|
||||
|
||||
this.isTyping = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
console.log('Specialist error event received:', eventData);
|
||||
|
||||
// Find the message with the matching task ID
|
||||
const messageIndex = this.allMessages.findIndex(msg =>
|
||||
msg.taskId === eventData.taskId
|
||||
);
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// Update the message to show error
|
||||
this.allMessages[messageIndex].content = eventData.message || 'Er is een fout opgetreden.';
|
||||
this.allMessages[messageIndex].type = 'error';
|
||||
this.allMessages[messageIndex].status = 'error';
|
||||
}
|
||||
|
||||
this.isTyping = false;
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
// UI helpers
|
||||
scrollToBottom() {
|
||||
if (this.$refs.messageHistory) {
|
||||
this.$refs.messageHistory.scrollToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
focusChatInput() {
|
||||
if (this.$refs.chatInput) {
|
||||
this.$refs.chatInput.focusInput();
|
||||
}
|
||||
},
|
||||
|
||||
// Search functionality
|
||||
searchMessages(query) {
|
||||
if (!query.trim()) {
|
||||
this.isSearching = false;
|
||||
this.filteredMessages = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
const searchTerm = query.toLowerCase();
|
||||
this.filteredMessages = this.allMessages.filter(message =>
|
||||
message.content &&
|
||||
message.content.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.isSearching = false;
|
||||
this.filteredMessages = [];
|
||||
this.messageSearch = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.chat-app-container {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -49,14 +49,6 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log('🔍 [DEBUG] LanguageSelector mounted with Vue SFC');
|
||||
console.log('🔍 [DEBUG] Props:', {
|
||||
initialLanguage: this.initialLanguage,
|
||||
currentLanguage: this.currentLanguage,
|
||||
supportedLanguageDetails: this.supportedLanguageDetails,
|
||||
allowedLanguages: this.allowedLanguages
|
||||
});
|
||||
|
||||
// Emit initial language
|
||||
this.$emit('language-changed', this.selectedLanguage);
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
|
||||
// Construct the SSE URL
|
||||
const baseUrl = window.location.origin;
|
||||
const sseUrl = `${baseUrl}${this.apiPrefix}/api/progress/${this.taskId}`;
|
||||
const sseUrl = `${baseUrl}${this.apiPrefix}/api/task_progress/${this.taskId}`;
|
||||
|
||||
console.log('SSE URL:', sseUrl);
|
||||
|
||||
@@ -112,19 +112,6 @@ export default {
|
||||
this.handleError(event);
|
||||
};
|
||||
|
||||
// Listen for specific event types
|
||||
this.eventSource.addEventListener('progress', (event) => {
|
||||
this.handleProgressUpdate(event);
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('complete', (event) => {
|
||||
this.handleSpecialistComplete(event);
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('error', (event) => {
|
||||
this.handleSpecialistError(event);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create EventSource:', error);
|
||||
this.error = 'Kan geen verbinding maken met de voortgangsstream.';
|
||||
@@ -145,8 +132,30 @@ export default {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Progress update:', data);
|
||||
|
||||
// Check voor processing_type om te bepalen welke handler te gebruiken
|
||||
if (data.processing_type === 'EveAI Specialist Complete') {
|
||||
console.log('Detected specialist complete via processing_type');
|
||||
this.handleSpecialistComplete(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check voor andere completion statuses en errors
|
||||
if (data.processing_type === 'EveAI Specialist Error')
|
||||
{
|
||||
console.log('Detected specialist error via processing_type or status');
|
||||
this.handleSpecialistError(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Voeg bericht toe aan progressLines
|
||||
if (data.message) {
|
||||
this.progressLines.push(data.message);
|
||||
} else if (data.data && data.data.message) {
|
||||
this.progressLines.push(data.data.message);
|
||||
} else if (data.processing_type) {
|
||||
// Gebruik processing_type als message wanneer er geen andere message is
|
||||
this.progressLines.push(`${data.processing_type}...`);
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom if expanded
|
||||
if (this.isExpanded) {
|
||||
@@ -157,7 +166,6 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing progress data:', error);
|
||||
}
|
||||
@@ -174,21 +182,36 @@ export default {
|
||||
this.connecting = false;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Emit the complete event to parent
|
||||
if (data.result && data.result.answer) {
|
||||
this.$emit('specialist-complete', {
|
||||
answer: data.result.answer,
|
||||
form_request: data.result.form_request,
|
||||
result: data.result,
|
||||
interactionId: data.interaction_id,
|
||||
taskId: this.taskId
|
||||
});
|
||||
} else {
|
||||
console.error('Missing result.answer in specialist complete data:', data);
|
||||
// Verschillende manieren om de result data te verkrijgen
|
||||
let resultData = null;
|
||||
let answer = null;
|
||||
let formRequest = null;
|
||||
let interactionId = null;
|
||||
|
||||
resultData = data.data.result
|
||||
console.log('Result data:', resultData);
|
||||
|
||||
if (resultData) {
|
||||
// Standaard format
|
||||
answer = resultData.answer;
|
||||
formRequest = resultData.form_request;
|
||||
interactionId = data.data.interaction_id;
|
||||
}
|
||||
this.$emit('specialist-complete', {
|
||||
answer: answer,
|
||||
form_request: formRequest,
|
||||
result: resultData,
|
||||
interactionId: interactionId,
|
||||
taskId: this.taskId
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error parsing specialist complete data:', error);
|
||||
this.handleSpecialistError({ data: JSON.stringify({ Error: 'Failed to parse completion data' }) });
|
||||
this.handleSpecialistError({
|
||||
data: JSON.stringify({
|
||||
Error: 'Failed to parse completion data',
|
||||
processing_type: 'EveAI Specialist Error'
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -208,15 +231,23 @@ export default {
|
||||
const errorMessage = "We could not process your request. Please try again later.";
|
||||
this.error = errorMessage;
|
||||
|
||||
// Extract error details from various possible locations
|
||||
const originalError =
|
||||
data.Error ||
|
||||
data.error ||
|
||||
data.message ||
|
||||
data.data?.error ||
|
||||
data.data?.Error ||
|
||||
data.data?.message ||
|
||||
'Unknown error';
|
||||
|
||||
// Log the actual error for debug purposes
|
||||
if (data.Error) {
|
||||
console.error('Specialist Error:', data.Error);
|
||||
}
|
||||
console.error('Specialist Error:', originalError);
|
||||
|
||||
// Emit error event to parent
|
||||
this.$emit('specialist-error', {
|
||||
message: errorMessage,
|
||||
originalError: data.Error,
|
||||
originalError: originalError,
|
||||
taskId: this.taskId
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -226,6 +257,13 @@ export default {
|
||||
this.hasError = true;
|
||||
this.connecting = false;
|
||||
this.disconnectEventSource();
|
||||
|
||||
// Emit generic error
|
||||
this.$emit('specialist-error', {
|
||||
message: 'Er is een onbekende fout opgetreden.',
|
||||
originalError: 'Failed to parse error data',
|
||||
taskId: this.taskId
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -236,13 +274,22 @@ export default {
|
||||
|
||||
// Try to parse error data
|
||||
try {
|
||||
if (event.data) {
|
||||
const errorData = JSON.parse(event.data);
|
||||
if (errorData && errorData.message) {
|
||||
this.error = errorData.message;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Keep generic error message if parsing fails
|
||||
}
|
||||
|
||||
// Emit error to parent
|
||||
this.$emit('specialist-error', {
|
||||
message: this.error,
|
||||
originalError: 'SSE Connection Error',
|
||||
taskId: this.taskId
|
||||
});
|
||||
},
|
||||
|
||||
toggleExpand() {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
autoScroll: {{ settings.auto_scroll|default('true')|lower }},
|
||||
allowReactions: {{ settings.allow_reactions|default('true')|lower }}
|
||||
},
|
||||
apiPrefix: '{{ request.headers.get("X-Forwarded-Prefix", "") }}',
|
||||
apiPrefix: '{{ request.headers.get("X-Forwarded-Prefix", "") }}/chat',
|
||||
language: '{{ session.magic_link.specialist_args.language|default("nl") }}',
|
||||
supportedLanguageDetails: {{ config.SUPPORTED_LANGUAGE_DETAILS|tojson|safe }},
|
||||
allowedLanguages: {{ tenant_make.allowed_languages|tojson|safe }},
|
||||
|
||||
@@ -139,31 +139,57 @@ def chat(magic_link_code):
|
||||
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||
return render_template('error.html', message="An error occurred while setting up the chat.")
|
||||
|
||||
|
||||
@chat_bp.route('/api/send_message', methods=['POST'])
|
||||
def send_message():
|
||||
"""
|
||||
API endpoint to send a message to the specialist
|
||||
"""
|
||||
current_app.logger.debug(f"Sending message to specialist: {session.get('specialist', {}).get('id', 'UNKNOWN')}\n"
|
||||
f"with data: {request.json} \n"
|
||||
f"BEFORE TRY")
|
||||
try:
|
||||
current_app.logger.debug(
|
||||
f"Sending message to specialist: {session.get('specialist', {}).get('id', 'UNKNOWN')}\n"
|
||||
f"with data: {request.json} \n"
|
||||
f"AFTER TRY")
|
||||
|
||||
# Voeg meer debug logging toe om elk stap te traceren
|
||||
current_app.logger.debug("Step 1: Getting request data")
|
||||
data = request.json
|
||||
message = data.get('message', '')
|
||||
form_values = data.get('form_values', {})
|
||||
|
||||
current_app.logger.debug(f"Step 2: Parsed message='{message}', form_values={form_values}")
|
||||
|
||||
# Controleer of er ofwel een bericht of formuliergegevens zijn
|
||||
if not message and not form_values:
|
||||
current_app.logger.debug("Step 3: No message or form data - returning error")
|
||||
return jsonify({'error': 'No message or form data provided'}), 400
|
||||
|
||||
tenant_id = session['tenant']['id']
|
||||
specialist_id = session['specialist']['id']
|
||||
current_app.logger.debug("Step 4: Getting session data")
|
||||
# Veiliger session toegang met fallbacks
|
||||
tenant_data = session.get('tenant', {})
|
||||
specialist_data = session.get('specialist', {})
|
||||
|
||||
tenant_id = tenant_data.get('id')
|
||||
specialist_id = specialist_data.get('id')
|
||||
chat_session_id = session.get('chat_session_id')
|
||||
specialist_args = session['magic_link'].get('specialist_args', {})
|
||||
specialist_args = session.get('magic_link', {}).get('specialist_args', {})
|
||||
|
||||
current_app.logger.debug(
|
||||
f"Step 5: Session data - tenant_id={tenant_id}, specialist_id={specialist_id}, chat_session_id={chat_session_id}")
|
||||
|
||||
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||
current_app.logger.error(
|
||||
f"Missing session data: tenant_id={tenant_id}, specialist_id={specialist_id}, chat_session_id={chat_session_id}")
|
||||
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||
|
||||
current_app.logger.debug("Step 6: Switching to tenant schema")
|
||||
# Switch to tenant schema
|
||||
Database(tenant_id).switch_schema()
|
||||
|
||||
current_app.logger.debug("Step 7: Preparing specialist arguments")
|
||||
# Add user message to specialist arguments
|
||||
if message:
|
||||
specialist_args['question'] = message
|
||||
@@ -177,11 +203,13 @@ def send_message():
|
||||
if user_language:
|
||||
specialist_args['language'] = user_language
|
||||
|
||||
current_app.logger.debug(f"Step 8: About to execute specialist with args: {specialist_args}")
|
||||
current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n"
|
||||
f" with args: {specialist_args}\n"
|
||||
f"with session ID: {chat_session_id}")
|
||||
|
||||
# Execute specialist
|
||||
current_app.logger.debug("Step 9: Calling SpecialistServices.execute_specialist")
|
||||
result = SpecialistServices.execute_specialist(
|
||||
tenant_id=tenant_id,
|
||||
specialist_id=specialist_id,
|
||||
@@ -190,17 +218,22 @@ def send_message():
|
||||
user_timezone=data.get('timezone', 'UTC')
|
||||
)
|
||||
|
||||
current_app.logger.debug(f"Specialist execution result: {result}")
|
||||
current_app.logger.debug(f"Step 10: Specialist execution result: {result}")
|
||||
|
||||
# Store the task ID for polling
|
||||
current_app.logger.debug("Step 11: Storing task ID in session")
|
||||
session['current_task_id'] = result['task_id']
|
||||
|
||||
return jsonify({
|
||||
current_app.logger.debug("Step 12: Preparing response")
|
||||
response_data = {
|
||||
'status': 'processing',
|
||||
'task_id': result['task_id'],
|
||||
'content': 'Verwerking gestart...',
|
||||
'type': 'text'
|
||||
})
|
||||
}
|
||||
|
||||
current_app.logger.debug(f"Step 13: Returning response: {response_data}")
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error sending message: {str(e)}", exc_info=True)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -11,17 +11,12 @@ import '../../../eveai_chat_client/static/assets/css/language-selector.css';
|
||||
// Dependencies
|
||||
import { createApp, version } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { FormField } from '../../../eveai_chat_client/static/assets/js/components/FormField.js';
|
||||
import { FormField } from '../../../../../../../../../Users/josako/Library/Application Support/JetBrains/PyCharm2025.1/scratches/old js files/FormField.js';
|
||||
|
||||
// Vue en andere bibliotheken beschikbaar maken
|
||||
window.Vue = { createApp, version };
|
||||
window.marked = marked;
|
||||
|
||||
// Debug: Check Vue build type
|
||||
console.log('🔍 [DEBUG] Vue object:', window.Vue);
|
||||
console.log('🔍 [DEBUG] Vue.createApp:', typeof window.Vue.createApp);
|
||||
console.log('🔍 [DEBUG] Vue.version:', window.Vue.version);
|
||||
|
||||
// Support tools
|
||||
import '../../../eveai_chat_client/static/assets/js/iconManager.js';
|
||||
import '../../../eveai_chat_client/static/assets/js/translation.js';
|
||||
@@ -35,7 +30,7 @@ console.log('Components loaded:', Object.keys(Components));
|
||||
|
||||
// Import specifieke componenten
|
||||
import LanguageSelector from '../../../eveai_chat_client/static/assets/vue-components/LanguageSelector.vue';
|
||||
import { ChatApp } from '../../../eveai_chat_client/static/assets/js/ChatApp.js';
|
||||
import ChatApp from '../../../eveai_chat_client/static/assets/vue-components/ChatApp.vue';
|
||||
|
||||
// Globale Vue error tracking
|
||||
window.addEventListener('error', function(event) {
|
||||
@@ -44,8 +39,6 @@ window.addEventListener('error', function(event) {
|
||||
|
||||
// Wacht tot DOM geladen is
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🔍 [DEBUG] DOM content loaded, initializing application...');
|
||||
|
||||
// Controleer of chatConfig is ingesteld
|
||||
if (!window.chatConfig) {
|
||||
console.error('chatConfig is niet beschikbaar');
|
||||
@@ -82,10 +75,7 @@ function fillSidebarExplanation() {
|
||||
* Initialiseert de language selector
|
||||
*/
|
||||
function initializeLanguageSelector() {
|
||||
console.log('🔍 [DEBUG] Start initializeLanguageSelector');
|
||||
|
||||
const container = document.getElementById('language-selector-container');
|
||||
console.log('🔍 [DEBUG] Container element:', container);
|
||||
|
||||
if (!container) {
|
||||
console.error('#language-selector-container niet gevonden');
|
||||
@@ -100,8 +90,6 @@ function initializeLanguageSelector() {
|
||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de']
|
||||
};
|
||||
|
||||
console.log('🔍 [DEBUG] Props voor LanguageSelector:', JSON.stringify(props, null, 2));
|
||||
|
||||
// Mount de component direct - BELANGRIJK: we gebruiken window.Vue als dat beschikbaar is
|
||||
// Dit is nodig voor compatibiliteit met oude code
|
||||
const app = window.Vue && typeof window.Vue.createApp === 'function'
|
||||
@@ -117,7 +105,6 @@ function initializeLanguageSelector() {
|
||||
|
||||
// Mount de component
|
||||
const mountedApp = app.mount(container);
|
||||
console.log('🔍 [DEBUG] LanguageSelector successfully mounted with Vue template:', mountedApp);
|
||||
|
||||
// Language change event listener
|
||||
document.addEventListener('vue:language-changed', function(event) {
|
||||
@@ -138,8 +125,6 @@ function initializeLanguageSelector() {
|
||||
// Sla voorkeur op
|
||||
localStorage.setItem('preferredLanguage', newLanguage);
|
||||
});
|
||||
|
||||
console.log('🔍 [DEBUG] Language selector setup voltooid');
|
||||
} catch (error) {
|
||||
console.error('🚨 [CRITICAL ERROR] Bij initialiseren language selector:', error);
|
||||
console.error('Stack trace:', error.stack);
|
||||
@@ -150,29 +135,15 @@ function initializeLanguageSelector() {
|
||||
* Initialiseert de chat app (Vue component)
|
||||
*/
|
||||
function initializeChatApp() {
|
||||
console.log('🔍 [DEBUG] Start initializeChatApp');
|
||||
|
||||
const container = document.querySelector('.chat-container');
|
||||
console.log('🔍 [DEBUG] Chat container element:', container);
|
||||
|
||||
if (!container) {
|
||||
console.error('.chat-container niet gevonden');
|
||||
console.error('🚨 [CRITICAL ERROR] .chat-container niet gevonden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Controleer of componenten beschikbaar zijn en log debug informatie
|
||||
console.log('🔍 [DEBUG] ChatApp component beschikbaar:', ChatApp);
|
||||
console.log('🔍 [DEBUG] Geïmporteerde componenten:', Object.keys(Components));
|
||||
console.log('🔍 [DEBUG] MessageHistory component:', Components.MessageHistory);
|
||||
console.log('🔍 [DEBUG] ChatInput component:', Components.ChatInput);
|
||||
|
||||
// Geen workarounds voor Popper nodig
|
||||
|
||||
// Components are now using pure Vue templates
|
||||
|
||||
if (!ChatApp) {
|
||||
throw new Error('ChatApp component niet gevonden');
|
||||
throw new Error('🚨 [CRITICAL ERROR] ChatApp component niet gevonden');
|
||||
}
|
||||
|
||||
// Extra verificatie dat alle sub-componenten beschikbaar zijn
|
||||
@@ -192,16 +163,48 @@ function initializeChatApp() {
|
||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de']
|
||||
};
|
||||
|
||||
console.log('🔍 [DEBUG] Alle componenten registreren voor ChatApp...');
|
||||
|
||||
console.log('🔍 [DEBUG] Props voor ChatApp:', JSON.stringify(props, null, 2));
|
||||
|
||||
// Mount de component met alle nodige componenten
|
||||
const app = createApp(ChatApp, props);
|
||||
|
||||
// SSE verbinding configuratie - injecteren in ChatApp component
|
||||
app.provide('sseConfig', {
|
||||
maxRetries: 3,
|
||||
retryDelay: 2000,
|
||||
handleSseError: function(error, taskId) {
|
||||
console.warn(`SSE verbinding voor task ${taskId} mislukt:`, error);
|
||||
// Fallback naar polling als SSE faalt
|
||||
return this.fallbackToPolling(taskId);
|
||||
},
|
||||
fallbackToPolling: function(taskId) {
|
||||
console.log(`Fallback naar polling voor task ${taskId}`);
|
||||
// Polling implementatie - elke 3 seconden status checken
|
||||
const pollingInterval = setInterval(() => {
|
||||
const endpoint = `${props.apiPrefix}/api/task_status/${taskId}`;
|
||||
fetch(endpoint)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`Task status endpoint error: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Dispatch event om dezelfde event interface als SSE te behouden
|
||||
const mockEvent = new CustomEvent('message', {
|
||||
detail: { data: JSON.stringify(data) }
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent(`sse:${taskId}:message`, { detail: mockEvent }));
|
||||
|
||||
// Stop polling als taak klaar is
|
||||
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
|
||||
clearInterval(pollingInterval);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Polling error:', err));
|
||||
}, 3000);
|
||||
return pollingInterval;
|
||||
}
|
||||
});
|
||||
|
||||
// Registreer alle componenten globaal
|
||||
Object.entries(Components).forEach(([name, component]) => {
|
||||
console.log(`🔍 [DEBUG] Registreer component globaal: ${name}`);
|
||||
app.component(name, component);
|
||||
});
|
||||
|
||||
@@ -212,25 +215,14 @@ function initializeChatApp() {
|
||||
console.error('Error Info:', info);
|
||||
};
|
||||
|
||||
// Log app object voor debugging
|
||||
console.log('🔍 [DEBUG] ChatApp Vue app object:', app);
|
||||
console.log('🔍 [DEBUG] App.mount functie:', typeof app.mount);
|
||||
|
||||
// Mount de component
|
||||
const mountedApp = app.mount(container);
|
||||
console.log('🔍 [DEBUG] ChatApp gemount, instance:', mountedApp);
|
||||
|
||||
// Bewaar referentie globaal voor debugging
|
||||
window.__chatApp = mountedApp;
|
||||
|
||||
// Bewaar een referentie naar de Vue instantie voor gebruik door andere componenten
|
||||
window.__vueApp = app;
|
||||
console.log('🔍 [DEBUG] Vue app instance globaal beschikbaar als window.__vueApp');
|
||||
|
||||
// Log belangrijke methods van de gemounte component
|
||||
console.log('🔍 [DEBUG] Belangrijke ChatApp methods beschikbaar:', {
|
||||
initializeChat: typeof mountedApp.initializeChat === 'function'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('🚨 [CRITICAL ERROR] Bij initialiseren chat app:', error);
|
||||
console.error('Stack trace:', error.stack);
|
||||
@@ -240,4 +232,37 @@ function initializeChatApp() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functie om Vue 3 element refs te verwerken
|
||||
* Oplossing voor $el.querySelector problemen
|
||||
*/
|
||||
window.getElementFromRef = function(ref) {
|
||||
if (!ref) return null;
|
||||
|
||||
// Vue 3 refs zijn reactieve objecten met een .value eigenschap
|
||||
if (ref.value && ref.value instanceof HTMLElement) {
|
||||
return ref.value;
|
||||
}
|
||||
|
||||
// Directe HTML element refs
|
||||
if (ref instanceof HTMLElement) {
|
||||
return ref;
|
||||
}
|
||||
|
||||
// Vue 3 component instance (geen $el meer in Vue 3)
|
||||
if (ref.$el && ref.$el instanceof HTMLElement) {
|
||||
return ref.$el;
|
||||
}
|
||||
|
||||
// Voor template refs van Vue 3 mounted componenten
|
||||
if (typeof ref === 'object' && ref !== null) {
|
||||
const element = ref.el || ref.$el || ref.value;
|
||||
if (element instanceof HTMLElement) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Chat client modules geladen en gebundeld met moderne ES module structuur.');
|
||||
2
nginx/static/dist/chat-client.css
vendored
2
nginx/static/dist/chat-client.css
vendored
File diff suppressed because one or more lines are too long
65
nginx/static/dist/chat-client.js
vendored
65
nginx/static/dist/chat-client.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user