- Min of meer werkende chat client new stule

This commit is contained in:
Josako
2025-07-20 11:36:00 +02:00
parent b60600e9f6
commit ccb844c15c
24 changed files with 735 additions and 3230 deletions

View File

@@ -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

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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>
`
};

View File

@@ -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';

View File

@@ -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

View 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>

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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 }},

View File

@@ -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.

View File

@@ -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.');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long