tussentijdse status voor significante wijzigingen. Bezig aan creatie Dynamic Form in de chat client.
This commit is contained in:
14
common/utils/cache/config_cache.py
vendored
14
common/utils/cache/config_cache.py
vendored
@@ -7,7 +7,7 @@ from flask import current_app
|
||||
|
||||
from common.utils.cache.base import CacheHandler, CacheKey
|
||||
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
||||
catalog_types, partner_service_types, processor_types, customisation_types
|
||||
catalog_types, partner_service_types, processor_types, customisation_types, specialist_form_types
|
||||
|
||||
|
||||
def is_major_minor(version: str) -> bool:
|
||||
@@ -478,6 +478,14 @@ CustomisationConfigCacheHandler, CustomisationConfigVersionTreeCacheHandler, Cus
|
||||
)
|
||||
)
|
||||
|
||||
SpecialistFormConfigCacheHandler, SpecialistFormConfigVersionTreeCacheHandler, SpecialistFormConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='specialist_forms',
|
||||
config_dir='config/specialist_forms',
|
||||
types_module=specialist_form_types.SPECIALIST_FORM_TYPES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||
@@ -513,6 +521,9 @@ def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.register_handler(CustomisationConfigCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(CustomisationConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(CustomisationConfigVersionTreeCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(SpecialistFormConfigCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(SpecialistFormConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(SpecialistFormConfigVersionTreeCacheHandler, 'eveai_config')
|
||||
|
||||
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
||||
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
||||
@@ -524,3 +535,4 @@ def register_config_cache_handlers(cache_manager) -> None:
|
||||
cache_manager.processors_config_cache.set_version_tree_cache(cache_manager.processors_version_tree_cache)
|
||||
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
||||
cache_manager.customisations_config_cache.set_version_tree_cache(cache_manager.customisations_version_tree_cache)
|
||||
cache_manager.specialist_forms_config_cache.set_version_tree_cache(cache_manager.specialist_forms_version_tree_cache)
|
||||
|
||||
@@ -67,7 +67,89 @@ class Config(object):
|
||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024
|
||||
|
||||
# supported languages
|
||||
SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es']
|
||||
SUPPORTED_LANGUAGES = ['en', 'fr', 'nl', 'de', 'es', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi']
|
||||
SUPPORTED_LANGUAGE_DETAILS = {
|
||||
"English": {
|
||||
"iso 639-1": "en",
|
||||
"iso 639-2": "eng",
|
||||
"iso 639-3": "eng",
|
||||
"flag": "🇬🇧"
|
||||
},
|
||||
"French": {
|
||||
"iso 639-1": "fr",
|
||||
"iso 639-2": "fre", # of 'fra'
|
||||
"iso 639-3": "fra",
|
||||
"flag": "🇫🇷"
|
||||
},
|
||||
"German": {
|
||||
"iso 639-1": "de",
|
||||
"iso 639-2": "ger", # of 'deu'
|
||||
"iso 639-3": "deu",
|
||||
"flag": "🇩🇪"
|
||||
},
|
||||
"Spanish": {
|
||||
"iso 639-1": "es",
|
||||
"iso 639-2": "spa",
|
||||
"iso 639-3": "spa",
|
||||
"flag": "🇪🇸"
|
||||
},
|
||||
"Italian": {
|
||||
"iso 639-1": "it",
|
||||
"iso 639-2": "ita",
|
||||
"iso 639-3": "ita",
|
||||
"flag": "🇮🇹"
|
||||
},
|
||||
"Portuguese": {
|
||||
"iso 639-1": "pt",
|
||||
"iso 639-2": "por",
|
||||
"iso 639-3": "por",
|
||||
"flag": "🇵🇹"
|
||||
},
|
||||
"Dutch": {
|
||||
"iso 639-1": "nl",
|
||||
"iso 639-2": "dut", # of 'nld'
|
||||
"iso 639-3": "nld",
|
||||
"flag": "🇳🇱"
|
||||
},
|
||||
"Russian": {
|
||||
"iso 639-1": "ru",
|
||||
"iso 639-2": "rus",
|
||||
"iso 639-3": "rus",
|
||||
"flag": "🇷🇺"
|
||||
},
|
||||
"Chinese": {
|
||||
"iso 639-1": "zh",
|
||||
"iso 639-2": "chi", # of 'zho'
|
||||
"iso 639-3": "zho",
|
||||
"flag": "🇨🇳"
|
||||
},
|
||||
"Japanese": {
|
||||
"iso 639-1": "ja",
|
||||
"iso 639-2": "jpn",
|
||||
"iso 639-3": "jpn",
|
||||
"flag": "🇯🇵"
|
||||
},
|
||||
"Korean": {
|
||||
"iso 639-1": "ko",
|
||||
"iso 639-2": "kor",
|
||||
"iso 639-3": "kor",
|
||||
"flag": "🇰🇷"
|
||||
},
|
||||
"Arabic": {
|
||||
"iso 639-1": "ar",
|
||||
"iso 639-2": "ara",
|
||||
"iso 639-3": "ara",
|
||||
"flag": "🇸🇦"
|
||||
},
|
||||
"Hindi": {
|
||||
"iso 639-1": "hi",
|
||||
"iso 639-2": "hin",
|
||||
"iso 639-3": "hin",
|
||||
"flag": "🇮🇳"
|
||||
},
|
||||
}
|
||||
|
||||
SUPPORTED_LANGUAGES_Full = list(SUPPORTED_LANGUAGE_DETAILS.keys())
|
||||
|
||||
# supported currencies
|
||||
SUPPORTED_CURRENCIES = ['€', '$']
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
version: "1.0.0"
|
||||
name: "Personal Contact Form"
|
||||
icon: "call"
|
||||
fields:
|
||||
name:
|
||||
name: "Name"
|
||||
description: "Your name"
|
||||
type: "str"
|
||||
required: true
|
||||
email:
|
||||
name: "Email"
|
||||
type: "str"
|
||||
description: "Your Name"
|
||||
required: true
|
||||
phone:
|
||||
name: "Phone Number"
|
||||
type: "str"
|
||||
description: "Your Phone Number"
|
||||
required: true
|
||||
Address:
|
||||
name: "Address"
|
||||
type: "text"
|
||||
description: "Your Address"
|
||||
required: false
|
||||
status:
|
||||
name: "Marital Status"
|
||||
type: "enum"
|
||||
description: "Your Marital Status"
|
||||
required: false
|
||||
default: "single"
|
||||
allowed_values:
|
||||
- "single"
|
||||
- "married"
|
||||
- "divorced"
|
||||
can_contact:
|
||||
name: "Allow Contact"
|
||||
type: "boolean"
|
||||
description: "Allow us to contact you?"
|
||||
required: true
|
||||
default: false
|
||||
7
config/type_defs/specialist_form_types.py
Normal file
7
config/type_defs/specialist_form_types.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# Specialist Form Types
|
||||
SPECIALIST_FORM_TYPES = {
|
||||
"PERSONAL_CONTACT_FORM": {
|
||||
"name": "Contact Form",
|
||||
"description": "A form for entering your personal contact details",
|
||||
},
|
||||
}
|
||||
@@ -408,22 +408,6 @@
|
||||
}
|
||||
|
||||
|
||||
/* Edit mode styling */
|
||||
.edit-mode .edit-textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
font-family: inherit;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 12px;
|
||||
|
||||
75
eveai_chat_client/static/css/form-message.css
Normal file
75
eveai_chat_client/static/css/form-message.css
Normal file
@@ -0,0 +1,75 @@
|
||||
/* Stijlen voor het FormMessage component in chatberichten */
|
||||
.form-message {
|
||||
background-color: #f5f8ff;
|
||||
border: 1px solid #e1e8f5;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e1e8f5;
|
||||
}
|
||||
|
||||
.form-message-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #4a6fa5;
|
||||
}
|
||||
|
||||
.form-message-title {
|
||||
font-weight: 600;
|
||||
color: #3a5a80;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-message-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-message-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.field-message-label {
|
||||
flex: 0 0 120px;
|
||||
font-weight: 500;
|
||||
color: #4a6fa5;
|
||||
font-size: 0.85rem;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.field-message-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.field-message-value.text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.form-message-field {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.field-message-label {
|
||||
flex: 0 0 100%;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.field-message-value {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
175
eveai_chat_client/static/css/form.css
Normal file
175
eveai_chat_client/static/css/form.css
Normal file
@@ -0,0 +1,175 @@
|
||||
/* Dynamisch formulier stijlen */
|
||||
.dynamic-form-container {
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.dynamic-form {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.form-fields {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-field input,
|
||||
.form-field select,
|
||||
.form-field textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field select:focus,
|
||||
.form-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.form-field textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.field-description {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 0.8rem;
|
||||
color: #777;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
color: #555;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-toggle-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.form-toggle-btn.active {
|
||||
color: #4a90e2;
|
||||
background-color: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #e53935;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Read-only form styling */
|
||||
.form-readonly {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.form-field-readonly {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
flex: 0 0 30%;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-value {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export const ChatApp = {
|
||||
isSubmittingForm: false,
|
||||
messageIdCounter: 1,
|
||||
formValues: {},
|
||||
currentInputFormData: null,
|
||||
|
||||
// API prefix voor endpoints
|
||||
apiPrefix: chatConfig.apiPrefix || '',
|
||||
@@ -298,6 +299,53 @@ export const ChatApp = {
|
||||
}
|
||||
},
|
||||
|
||||
async submitFormFromInput(formValues) {
|
||||
this.isSubmittingForm = true;
|
||||
|
||||
if (!this.currentInputFormData) {
|
||||
console.error('No form data available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Submitting form from input:', this.currentInputFormData.title, formValues);
|
||||
|
||||
try {
|
||||
const response = await this.callAPI('/api/submit_form', {
|
||||
formData: formValues,
|
||||
formType: this.currentInputFormData.title,
|
||||
conversation_id: this.conversationId,
|
||||
user_id: this.userId
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.addMessage(
|
||||
`✅ ${response.message || 'Formulier succesvol verzonden!'}`,
|
||||
'ai',
|
||||
'text'
|
||||
);
|
||||
|
||||
// Wis het huidige formulier
|
||||
this.currentInputFormData = null;
|
||||
} else {
|
||||
this.addMessage(
|
||||
`❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`,
|
||||
'ai',
|
||||
'text'
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
this.addMessage(
|
||||
'Sorry, er ging iets mis bij het verzenden van het formulier. Probeer het opnieuw.',
|
||||
'ai',
|
||||
'text'
|
||||
);
|
||||
} finally {
|
||||
this.isSubmittingForm = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Message actions
|
||||
|
||||
retryMessage(messageId) {
|
||||
@@ -387,6 +435,56 @@ export const ChatApp = {
|
||||
this.filteredMessages = [];
|
||||
},
|
||||
|
||||
// Event handlers voor specialist events
|
||||
handleSpecialistComplete(eventData) {
|
||||
console.log('ChatApp received specialist-complete:', eventData);
|
||||
|
||||
// Als er een form_request is, voeg deze toe als nieuw bericht
|
||||
if (eventData.form_request) {
|
||||
console.log('Adding form request as new message:', eventData.form_request);
|
||||
|
||||
// Converteer de form_request naar het verwachte formaat
|
||||
const formData = this.convertFormRequest(eventData.form_request);
|
||||
|
||||
// Voeg het formulier toe als een nieuw AI bericht
|
||||
this.addMessage('', 'ai', 'form', formData);
|
||||
}
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
console.log('ChatApp received specialist-error:', eventData);
|
||||
|
||||
// Voeg foutbericht toe
|
||||
this.addMessage(
|
||||
eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.',
|
||||
'ai',
|
||||
'error'
|
||||
);
|
||||
},
|
||||
|
||||
// Helper methode om form_request te converteren naar het verwachte formaat
|
||||
convertFormRequest(formRequest) {
|
||||
console.log('Converting form request:', formRequest);
|
||||
|
||||
// Converteer de fields van object naar array formaat
|
||||
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
|
||||
id: fieldId,
|
||||
name: fieldDef.name,
|
||||
type: fieldDef.type,
|
||||
description: fieldDef.description,
|
||||
required: fieldDef.required || false,
|
||||
defaultValue: fieldDef.default || '',
|
||||
allowedValues: fieldDef.allowed_values || null
|
||||
}));
|
||||
|
||||
return {
|
||||
title: formRequest.name,
|
||||
icon: formRequest.icon || 'form',
|
||||
version: formRequest.version || '1.0',
|
||||
fields: fieldsArray
|
||||
};
|
||||
},
|
||||
|
||||
// Event handlers
|
||||
handleResize() {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
@@ -450,11 +548,54 @@ export const ChatApp = {
|
||||
this.$refs.searchInput?.focus();
|
||||
},
|
||||
|
||||
handleSpecialistError(errorData) {
|
||||
console.error('Specialist error:', errorData);
|
||||
// Als we willen kunnen we hier nog extra logica toevoegen, zoals statistieken bijhouden of centraal loggen
|
||||
},
|
||||
handleSpecialistComplete(eventData) {
|
||||
console.log('ChatApp received specialist-complete:', eventData);
|
||||
|
||||
// Als er een form_request is, stuur deze naar de ChatInput component
|
||||
if (eventData.form_request) {
|
||||
console.log('Providing form request to ChatInput:', eventData.form_request);
|
||||
|
||||
// Converteer de form_request naar het verwachte formaat
|
||||
const formData = this.convertFormRequest(eventData.form_request);
|
||||
|
||||
// Update de currentInputFormData voor ChatInput
|
||||
this.currentInputFormData = formData;
|
||||
}
|
||||
},
|
||||
|
||||
handleSpecialistError(eventData) {
|
||||
console.log('ChatApp received specialist-error:', eventData);
|
||||
|
||||
// Voeg foutbericht toe
|
||||
this.addMessage(
|
||||
eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.',
|
||||
'ai',
|
||||
'error'
|
||||
);
|
||||
},
|
||||
|
||||
// Helper methode om form_request te converteren naar het verwachte formaat
|
||||
convertFormRequest(formRequest) {
|
||||
console.log('Converting form request:', formRequest);
|
||||
|
||||
// Converteer de fields van object naar array formaat
|
||||
const fieldsArray = Object.entries(formRequest.fields || {}).map(([fieldId, fieldDef]) => ({
|
||||
id: fieldId,
|
||||
name: fieldDef.name,
|
||||
type: fieldDef.type,
|
||||
description: fieldDef.description,
|
||||
required: fieldDef.required || false,
|
||||
defaultValue: fieldDef.default || '',
|
||||
allowedValues: fieldDef.allowed_values || null
|
||||
}));
|
||||
|
||||
return {
|
||||
title: formRequest.name,
|
||||
icon: formRequest.icon || 'form',
|
||||
version: formRequest.version || '1.0',
|
||||
fields: fieldsArray
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
@@ -469,6 +610,7 @@ export const ChatApp = {
|
||||
:auto-scroll="true"
|
||||
@submit-form="submitForm"
|
||||
@specialist-error="handleSpecialistError"
|
||||
@specialist-complete="handleSpecialistComplete"
|
||||
ref="messageHistory"
|
||||
class="chat-messages-area"
|
||||
></message-history>
|
||||
@@ -480,10 +622,12 @@ export const ChatApp = {
|
||||
: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>
|
||||
@@ -493,12 +637,21 @@ export const ChatApp = {
|
||||
|
||||
};
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Zorg ervoor dat alle componenten correct geïnitialiseerd zijn voordat ze worden gebruikt
|
||||
const initializeApp = () => {
|
||||
console.log('Initializing Chat Application');
|
||||
|
||||
// Get access to the existing Vue app instance
|
||||
if (window.__vueApp) {
|
||||
// Zorg ervoor dat alle componenten globaal beschikbaar zijn via window
|
||||
window.TypingIndicator = TypingIndicator;
|
||||
window.FormField = FormField;
|
||||
window.DynamicForm = DynamicForm;
|
||||
window.ChatMessage = ChatMessage;
|
||||
window.MessageHistory = MessageHistory;
|
||||
window.ChatInput = ChatInput;
|
||||
window.ProgressTracker = ProgressTracker;
|
||||
|
||||
// Register ALL components globally
|
||||
window.__vueApp.component('TypingIndicator', TypingIndicator);
|
||||
window.__vueApp.component('FormField', FormField);
|
||||
@@ -520,4 +673,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
console.error('No existing Vue instance found on window.__vueApp');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
export const ChatInput = {
|
||||
name: 'ChatInput',
|
||||
components: {
|
||||
'dynamic-form': window.__vueApp ? DynamicForm : null
|
||||
},
|
||||
props: {
|
||||
currentMessage: {
|
||||
type: String,
|
||||
@@ -19,11 +22,38 @@ export const ChatInput = {
|
||||
type: Number,
|
||||
default: 2000
|
||||
},
|
||||
formData: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
emits: ['send-message', 'update-message', 'submit-form'],
|
||||
watch: {
|
||||
formData: {
|
||||
handler(newFormData) {
|
||||
console.log('ChatInput received formData:', newFormData);
|
||||
if (newFormData) {
|
||||
this.formValues = {}; // Reset formulierwaarden
|
||||
this.showForm = true;
|
||||
} else {
|
||||
this.showForm = false;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
currentMessage(newVal) {
|
||||
this.localMessage = newVal;
|
||||
},
|
||||
localMessage(newVal) {
|
||||
this.$emit('update-message', newVal);
|
||||
this.autoResize();
|
||||
}
|
||||
},
|
||||
emits: ['send-message', 'update-message'],
|
||||
data() {
|
||||
return {
|
||||
localMessage: this.currentMessage,
|
||||
formValues: {},
|
||||
showForm: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -35,19 +65,21 @@ export const ChatInput = {
|
||||
return this.characterCount > this.maxLength;
|
||||
},
|
||||
|
||||
canSend() {
|
||||
return this.localMessage.trim() &&
|
||||
!this.isLoading &&
|
||||
!this.isOverLimit;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentMessage(newVal) {
|
||||
this.localMessage = newVal;
|
||||
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));
|
||||
},
|
||||
localMessage(newVal) {
|
||||
this.$emit('update-message', newVal);
|
||||
this.autoResize();
|
||||
|
||||
canSend() {
|
||||
const hasValidForm = this.showForm && this.formData && this.validateForm();
|
||||
const hasValidMessage = this.localMessage.trim() && !this.isOverLimit;
|
||||
|
||||
return (!this.isLoading) && (hasValidForm || hasValidMessage);
|
||||
},
|
||||
|
||||
sendButtonText() {
|
||||
return this.showForm ? 'Verstuur formulier' : 'Verstuur bericht';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -64,11 +96,51 @@ export const ChatInput = {
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
if (this.canSend) {
|
||||
if (!this.canSend) return;
|
||||
|
||||
if (this.showForm && this.formData) {
|
||||
// Valideer het formulier
|
||||
if (this.validateForm()) {
|
||||
// Verstuur het formulier
|
||||
this.$emit('submit-form', this.formValues);
|
||||
this.formValues = {};
|
||||
// Reset het formulier na verzenden
|
||||
this.showForm = false;
|
||||
}
|
||||
} else if (this.localMessage.trim()) {
|
||||
// Verstuur normaal bericht
|
||||
this.$emit('send-message');
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
@@ -86,10 +158,44 @@ export const ChatInput = {
|
||||
clearInput() {
|
||||
this.localMessage = '';
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
toggleForm() {
|
||||
this.showForm = !this.showForm;
|
||||
if (!this.showForm) {
|
||||
this.focusInput();
|
||||
}
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
if (this.canSubmitForm) {
|
||||
this.$emit('submit-form', { ...this.formValues });
|
||||
this.showForm = false;
|
||||
this.focusInput();
|
||||
}
|
||||
},
|
||||
|
||||
cancelForm() {
|
||||
this.showForm = false;
|
||||
this.focusInput();
|
||||
},
|
||||
|
||||
updateFormValues(newValues) {
|
||||
this.formValues = { ...newValues };
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="chat-input-container">
|
||||
<div v-if="formData && showForm" class="dynamic-form-container">
|
||||
<dynamic-form
|
||||
:form-data="formData"
|
||||
:form-values="formValues"
|
||||
:is-submitting="isLoading"
|
||||
:hide-actions="true"
|
||||
@update:form-values="updateFormValues"
|
||||
></dynamic-form>
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<!-- Main input area -->
|
||||
<div class="input-main">
|
||||
@@ -104,21 +210,34 @@ export const ChatInput = {
|
||||
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">
|
||||
<!-- Formulier toggle knop -->
|
||||
<button
|
||||
v-if="hasFormData"
|
||||
@click="toggleForm"
|
||||
class="form-toggle-btn"
|
||||
:disabled="isLoading"
|
||||
:class="{ 'active': showForm }"
|
||||
:title="showForm ? 'Verberg formulier' : 'Toon formulier'"
|
||||
>
|
||||
<i class="material-icons">description</i>
|
||||
</button>
|
||||
|
||||
<!-- Send button -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
class="send-btn"
|
||||
:class="{ 'form-mode': showForm && formData }"
|
||||
:disabled="!canSend"
|
||||
:title="isOverLimit ? 'Bericht te lang' : 'Bericht verzenden'"
|
||||
:title="showForm ? '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">
|
||||
|
||||
@@ -24,8 +24,6 @@ export const ChatMessage = {
|
||||
emits: ['submit-form', 'image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
editedContent: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -75,22 +73,6 @@ export const ChatMessage = {
|
||||
.replace(/\n/g, '<br>');
|
||||
},
|
||||
|
||||
startEdit() {
|
||||
this.editedContent = this.message.content;
|
||||
this.isEditing = true;
|
||||
},
|
||||
|
||||
saveEdit() {
|
||||
// Implementatie van bewerkingen zou hier komen
|
||||
this.message.content = this.editedContent;
|
||||
this.isEditing = false;
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.isEditing = false;
|
||||
this.editedContent = '';
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
this.$emit('submit-form', this.message.formData, this.message.id);
|
||||
},
|
||||
@@ -125,23 +107,8 @@ export const ChatMessage = {
|
||||
@specialist-error="handleSpecialistError"
|
||||
></progress-tracker>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<div v-if="isEditing" class="edit-mode">
|
||||
<textarea
|
||||
v-model="editedContent"
|
||||
class="edit-textarea"
|
||||
rows="3"
|
||||
@keydown.enter.ctrl="saveEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
></textarea>
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit" class="btn-small btn-primary">Opslaan</button>
|
||||
<button @click="cancelEdit" class="btn-small btn-secondary">Annuleren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View mode -->
|
||||
<div v-else>
|
||||
<div>
|
||||
<div
|
||||
v-html="formatMessage(message.content)"
|
||||
class="message-text"
|
||||
|
||||
@@ -5,64 +5,173 @@ export const DynamicForm = {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (formData) => {
|
||||
return formData.title && formData.fields && Array.isArray(formData.fields);
|
||||
return formData && formData.title && formData.fields &&
|
||||
(Array.isArray(formData.fields) || typeof formData.fields === 'object');
|
||||
}
|
||||
},
|
||||
formValues: {
|
||||
type: Object,
|
||||
required: true
|
||||
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) {
|
||||
this.localFormValues = { ...newValues };
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
localFormValues: {
|
||||
handler(newValues) {
|
||||
this.$emit('update:formValues', newValues);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
emits: ['submit', 'cancel'],
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
// Basic validation
|
||||
const requiredFields = this.formData.fields.filter(field => field.required);
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const value = this.formValues[field.name];
|
||||
return !value || (typeof value === 'string' && !value.trim());
|
||||
});
|
||||
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.map(f => f.label).join(', ');
|
||||
const fieldNames = missingFields.join(', ');
|
||||
alert(`De volgende velden zijn verplicht: ${fieldNames}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('submit');
|
||||
this.$emit('submit', this.localFormValues);
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
updateFieldValue(fieldName, value) {
|
||||
// Emit an update for reactive binding
|
||||
this.$emit('update-field', fieldName, value);
|
||||
updateFieldValue(fieldId, value) {
|
||||
this.localFormValues[fieldId] = value;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="dynamic-form">
|
||||
<div class="form-title">{{ formData.title }}</div>
|
||||
|
||||
<div v-if="formData.description" class="form-description">
|
||||
{{ formData.description }}
|
||||
<div class="dynamic-form" :class="{ 'read-only': readOnly }">
|
||||
<div class="form-header" v-if="formData.title || formData.icon">
|
||||
<div class="form-icon" v-if="formData.icon">
|
||||
<i class="material-icons">{{ formData.icon }}</i>
|
||||
</div>
|
||||
<div class="form-title">{{ formData.title }}</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" novalidate>
|
||||
<form-field
|
||||
v-for="field in formData.fields"
|
||||
:key="field.name"
|
||||
:field="field"
|
||||
:model-value="formValues[field.name]"
|
||||
@update:model-value="formValues[field.name] = $event"
|
||||
></form-field>
|
||||
|
||||
<div class="form-actions">
|
||||
|
||||
<div v-if="readOnly" class="form-readonly">
|
||||
<!-- Array-based fields -->
|
||||
<template v-if="Array.isArray(formData.fields)">
|
||||
<div v-for="field in formData.fields" :key="field.id || field.name" class="form-field-readonly">
|
||||
<div class="field-label">{{ field.name }}:</div>
|
||||
<div class="field-value">
|
||||
<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 === 'boolean'">
|
||||
{{ localFormValues[field.id || field.name] ? 'Ja' : 'Nee' }}
|
||||
</template>
|
||||
<template v-else-if="field.type === 'text'">
|
||||
<div class="text-value">{{ localFormValues[field.id || field.name] || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ localFormValues[field.id || field.name] || field.default || '-' }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Object-based fields -->
|
||||
<template v-else>
|
||||
<div v-for="(field, fieldId) in formData.fields" :key="fieldId" class="form-field-readonly">
|
||||
<div class="field-label">{{ field.name }}:</div>
|
||||
<div class="field-value">
|
||||
<template v-if="field.type === 'enum' && (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">{{ localFormValues[fieldId] || '-' }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ localFormValues[fieldId] || field.default || '-' }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" novalidate>
|
||||
<div class="form-fields">
|
||||
<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"
|
||||
@@ -72,7 +181,7 @@ export const DynamicForm = {
|
||||
<span v-if="isSubmitting" class="spinner"></span>
|
||||
{{ isSubmitting ? 'Verzenden...' : (formData.submitText || 'Versturen') }}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@@ -81,28 +190,6 @@ export const DynamicForm = {
|
||||
>
|
||||
{{ formData.cancelText || 'Annuleren' }}
|
||||
</button>
|
||||
|
||||
<!-- Optional reset button -->
|
||||
<button
|
||||
v-if="formData.showReset"
|
||||
type="reset"
|
||||
class="btn btn-outline"
|
||||
@click="$emit('reset')"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress indicator for multi-step forms -->
|
||||
<div v-if="formData.steps && formData.currentStep" class="form-progress">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: (formData.currentStep / formData.steps * 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<small>Stap {{ formData.currentStep }} van {{ formData.steps }}</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -5,22 +5,53 @@ export const FormField = {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (field) => {
|
||||
return field.name && field.type && field.label;
|
||||
return field.name && field.type;
|
||||
}
|
||||
},
|
||||
fieldId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
default: ''
|
||||
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) {
|
||||
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',
|
||||
'boolean': 'checkbox'
|
||||
};
|
||||
return typeMap[this.field.type] || this.field.type;
|
||||
},
|
||||
stepValue() {
|
||||
return this.field.type === 'float' ? 'any' : 1;
|
||||
},
|
||||
description() {
|
||||
return this.field.description || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -33,147 +64,83 @@ export const FormField = {
|
||||
},
|
||||
template: `
|
||||
<div class="form-field">
|
||||
<label>
|
||||
{{ field.label }}
|
||||
<label :for="fieldId">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="required">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Text/Email/Tel inputs -->
|
||||
|
||||
<!-- Tekstinvoer (string/str) -->
|
||||
<input
|
||||
v-if="['text', 'email', 'tel', 'url', 'password'].includes(field.type)"
|
||||
:type="field.type"
|
||||
v-if="fieldType === 'text'"
|
||||
:id="fieldId"
|
||||
type="text"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:maxlength="field.maxLength"
|
||||
:minlength="field.minLength"
|
||||
:title="description"
|
||||
>
|
||||
|
||||
<!-- Number input -->
|
||||
|
||||
<!-- Numerieke invoer (int/float) -->
|
||||
<input
|
||||
v-if="field.type === 'number'"
|
||||
v-if="fieldType === 'number'"
|
||||
:id="fieldId"
|
||||
type="number"
|
||||
v-model.number="value"
|
||||
:required="field.required"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step || 1"
|
||||
:step="stepValue"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:title="description"
|
||||
>
|
||||
|
||||
<!-- Date input -->
|
||||
<input
|
||||
v-if="field.type === 'date'"
|
||||
type="date"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
>
|
||||
|
||||
<!-- Time input -->
|
||||
<input
|
||||
v-if="field.type === 'time'"
|
||||
type="time"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
>
|
||||
|
||||
<!-- File input -->
|
||||
<input
|
||||
v-if="field.type === 'file'"
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
:required="field.required"
|
||||
:accept="field.accept"
|
||||
:multiple="field.multiple"
|
||||
>
|
||||
|
||||
<!-- Select dropdown -->
|
||||
<select
|
||||
v-if="field.type === 'select'"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
>
|
||||
<option value="">{{ field.placeholder || 'Kies een optie' }}</option>
|
||||
<option
|
||||
v-for="option in field.options"
|
||||
:key="option.value || option"
|
||||
:value="option.value || option"
|
||||
>
|
||||
{{ option.label || option }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Radio buttons -->
|
||||
<div v-if="field.type === 'radio'" class="radio-group">
|
||||
<label
|
||||
v-for="option in field.options"
|
||||
:key="option.value || option"
|
||||
class="radio-label"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:value="option.value || option"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
>
|
||||
<span>{{ option.label || option }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Checkboxes -->
|
||||
<div v-if="field.type === 'checkbox'" class="checkbox-group">
|
||||
<label
|
||||
v-for="option in field.options"
|
||||
:key="option.value || option"
|
||||
class="checkbox-label"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="option.value || option"
|
||||
v-model="value"
|
||||
>
|
||||
<span>{{ option.label || option }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Single checkbox -->
|
||||
<label v-if="field.type === 'single-checkbox'" class="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
>
|
||||
<span>{{ field.checkboxText || field.label }}</span>
|
||||
</label>
|
||||
|
||||
<!-- Textarea -->
|
||||
|
||||
<!-- Tekstvlak (text) -->
|
||||
<textarea
|
||||
v-if="field.type === 'textarea'"
|
||||
v-if="fieldType === 'textarea'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:rows="field.rows || 3"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:maxlength="field.maxLength"
|
||||
:title="description"
|
||||
></textarea>
|
||||
|
||||
<!-- Range slider -->
|
||||
<div v-if="field.type === 'range'" class="range-field">
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="value"
|
||||
:min="field.min || 0"
|
||||
:max="field.max || 100"
|
||||
:step="field.step || 1"
|
||||
|
||||
<!-- Dropdown (enum) -->
|
||||
<select
|
||||
v-if="fieldType === 'select'"
|
||||
:id="fieldId"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
>
|
||||
<option v-if="!field.required" value="">Selecteer een optie</option>
|
||||
<option
|
||||
v-for="option in (field.allowedValues || field.allowed_values || [])"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
<span class="range-value">{{ value }}</span>
|
||||
{{ 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)" class="field-error">
|
||||
Geen opties beschikbaar voor dit veld.
|
||||
<pre style="font-size: 10px; color: #999;">{{ JSON.stringify(field, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Help text -->
|
||||
<small v-if="field.helpText" class="help-text">
|
||||
{{ field.helpText }}
|
||||
</small>
|
||||
|
||||
<!-- Checkbox (boolean) -->
|
||||
<div v-if="fieldType === 'checkbox'" class="checkbox-container">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
:id="fieldId"
|
||||
type="checkbox"
|
||||
v-model="value"
|
||||
:required="field.required"
|
||||
:title="description"
|
||||
>
|
||||
<span class="checkbox-text">{{ field.description || 'Ja' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Geen beschrijving meer tonen, alleen als tooltip die al gedefinieerd is in de inputs -->
|
||||
</div>
|
||||
`
|
||||
};
|
||||
59
eveai_chat_client/static/js/components/FormMessage.js
Normal file
59
eveai_chat_client/static/js/components/FormMessage.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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>
|
||||
`
|
||||
};
|
||||
@@ -134,6 +134,8 @@ export const MessageHistory = {
|
||||
:api-prefix="apiPrefix"
|
||||
@submit-form="handleSubmitForm"
|
||||
@image-loaded="handleImageLoaded"
|
||||
@specialist-complete="$emit('specialist-complete', $event)"
|
||||
@specialist-error="$emit('specialist-error', $event)"
|
||||
></chat-message>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -204,9 +204,10 @@ export const ProgressTracker = {
|
||||
console.error('Error updating parent message:', err);
|
||||
}
|
||||
|
||||
// Emit event to parent met alle relevante data
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from eveai_chat_workers.retrievers.retriever_typing import RetrieverArguments
|
||||
from common.extensions import cache_manager
|
||||
@@ -21,6 +21,16 @@ class SpecialistArguments(BaseModel):
|
||||
"extra": "allow"
|
||||
}
|
||||
|
||||
# Structural optional fields available for all specialists
|
||||
question: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional question directed to the specialist"
|
||||
)
|
||||
form_values: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Optional form values filled by the user, keyed by field name"
|
||||
)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_required_arguments(self) -> 'SpecialistArguments':
|
||||
"""Validate that all required arguments for this specialist type are present"""
|
||||
@@ -91,6 +101,16 @@ class SpecialistResult(BaseModel):
|
||||
"extra": "allow"
|
||||
}
|
||||
|
||||
# Structural optional fields available for all specialists
|
||||
answer: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional textual answer from the specialist"
|
||||
)
|
||||
form_request: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Optional form definition to request user input"
|
||||
)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_required_results(self) -> 'SpecialistResult':
|
||||
"""Validate that all required result fields for this specialist type are present"""
|
||||
|
||||
@@ -17,6 +17,7 @@ from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, S
|
||||
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
|
||||
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||
from common.services.interaction.specialist_services import SpecialistServices
|
||||
from common.extensions import cache_manager
|
||||
|
||||
|
||||
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||
@@ -84,15 +85,18 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||
# results.competencies = flow_state.competencies
|
||||
|
||||
# self.create_selection_specialist(arguments, flow_state.competencies)
|
||||
for i in range(5):
|
||||
sleep(3)
|
||||
for i in range(3):
|
||||
sleep(1)
|
||||
self.ept.send_update(self.task_id, "Traicie Selection Specialist Processing", {"name": f"Processing Iteration {i}"})
|
||||
|
||||
# flow_results = asyncio.run(self.flow.kickoff_async(inputs=arguments.model_dump()))
|
||||
# flow_state = self.flow.state
|
||||
# results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||
contact_form = cache_manager.specialist_forms_config_cache.get_config("PERSONAL_CONTACT_FORM", "1.0")
|
||||
current_app.logger.debug(f"Contact form: {contact_form}")
|
||||
results = SpecialistResult.create_for_type(self.type, self.type_version,
|
||||
answer=f"Antwoord op uw vraag: {arguments.question}")
|
||||
answer=f"Antwoord op uw vraag: {arguments.question}",
|
||||
form_request=contact_form)
|
||||
|
||||
self.log_tuning(f"Traicie Selection Specialist execution ended", {"Results": results.model_dump()})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user