diff --git a/common/utils/cache/config_cache.py b/common/utils/cache/config_cache.py index dd7c267..c72c19d 100644 --- a/common/utils/cache/config_cache.py +++ b/common/utils/cache/config_cache.py @@ -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) diff --git a/config/config.py b/config/config.py index cde13f1..6c677d2 100644 --- a/config/config.py +++ b/config/config.py @@ -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 = ['โฌ', '$'] diff --git a/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml new file mode 100644 index 0000000..7e5a02d --- /dev/null +++ b/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml @@ -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 \ No newline at end of file diff --git a/config/type_defs/specialist_form_types.py b/config/type_defs/specialist_form_types.py new file mode 100644 index 0000000..57079e4 --- /dev/null +++ b/config/type_defs/specialist_form_types.py @@ -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", + }, +} \ No newline at end of file diff --git a/eveai_chat_client/static/css/chat-components.css b/eveai_chat_client/static/css/chat-components.css index 7f0df41..edb070c 100644 --- a/eveai_chat_client/static/css/chat-components.css +++ b/eveai_chat_client/static/css/chat-components.css @@ -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; diff --git a/eveai_chat_client/static/css/form-message.css b/eveai_chat_client/static/css/form-message.css new file mode 100644 index 0000000..6b3b636 --- /dev/null +++ b/eveai_chat_client/static/css/form-message.css @@ -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; + } +} diff --git a/eveai_chat_client/static/css/form.css b/eveai_chat_client/static/css/form.css new file mode 100644 index 0000000..3992aa2 --- /dev/null +++ b/eveai_chat_client/static/css/form.css @@ -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; +} diff --git a/eveai_chat_client/static/js/chat-app.js b/eveai_chat_client/static/js/chat-app.js index cbbf87d..4e547b4 100644 --- a/eveai_chat_client/static/js/chat-app.js +++ b/eveai_chat_client/static/js/chat-app.js @@ -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" > @@ -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" > @@ -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'); } -}); \ No newline at end of file +}; + +// Initialize app when DOM is ready +document.addEventListener('DOMContentLoaded', initializeApp); \ No newline at end of file diff --git a/eveai_chat_client/static/js/components/ChatInput.js b/eveai_chat_client/static/js/components/ChatInput.js index 139baaa..446feb1 100644 --- a/eveai_chat_client/static/js/components/ChatInput.js +++ b/eveai_chat_client/static/js/components/ChatInput.js @@ -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: `