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 index 7e5a02d..6eee03f 100644 --- a/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml +++ b/config/specialist_forms/globals/PERSONAL_CONTACT_FORM/1.0.0.yaml @@ -1,6 +1,7 @@ +type: "PERSONAL_CONTACT_FORM" version: "1.0.0" name: "Personal Contact Form" -icon: "call" +icon: "person" fields: name: name: "Name" @@ -17,24 +18,28 @@ fields: type: "str" description: "Your Phone Number" required: true - Address: + address: name: "Address" - type: "text" + type: "string" description: "Your Address" required: false - status: - name: "Marital Status" - type: "enum" - description: "Your Marital Status" + zip: + name: "Postal Code" + type: "string" + description: "Postal Code" required: false - default: "single" - allowed_values: - - "single" - - "married" - - "divorced" - can_contact: - name: "Allow Contact" + city: + name: "City" + type: "string" + description: "City" + required: false + country: + name: "Country" + type: "string" + description: "Country" + required: false + consent: + name: "Consent" type: "boolean" - description: "Allow us to contact you?" + description: "Consent" required: true - default: false \ No newline at end of file diff --git a/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml new file mode 100644 index 0000000..cbcf484 --- /dev/null +++ b/config/specialist_forms/globals/PROFESSIONAL_CONTACT_FORM/1.0.0.yaml @@ -0,0 +1,55 @@ +type: "PROFESSIONAL_CONTACT_FORM" +version: "1.0.0" +name: "Professional Contact Form" +icon: "account_circle" +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 + company: + name: "Company Name" + type: "str" + description: "Company Name" + required: true + job_title: + name: "Job Title" + type: "str" + description: "Job Title" + required: false + address: + name: "Address" + type: "str" + description: "Your Address" + required: false + zip: + name: "Postal Code" + type: "str" + description: "Postal Code" + required: false + city: + name: "City" + type: "str" + description: "City" + required: false + country: + name: "Country" + type: "str" + description: "Country" + required: false + consent: + name: "Consent" + type: "bool" + description: "Consent" + required: true diff --git a/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml new file mode 100644 index 0000000..55389ff --- /dev/null +++ b/config/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1.2.0.yaml @@ -0,0 +1,120 @@ +version: "1.1.0" +name: "Traicie Selection Specialist" +framework: "crewai" +partner: "traicie" +chat: false +configuration: + name: + name: "Name" + description: "The name the specialist is called upon." + type: "str" + required: true + role_reference: + name: "Role Reference" + description: "A customer reference to the role" + type: "str" + required: false + make: + name: "Make" + description: "The make for which the role is defined and the selection specialist is created" + type: "system" + system_name: "tenant_make" + required: true + competencies: + name: "Competencies" + description: "An ordered list of competencies." + type: "ordered_list" + list_type: "competency_details" + required: true + tone_of_voice: + name: "Tone of Voice" + description: "The tone of voice the specialist uses to communicate" + type: "enum" + allowed_values: ["Professional & Neutral", "Warm & Empathetic", "Energetic & Enthusiastic", "Accessible & Informal", "Expert & Trustworthy", "No-nonsense & Goal-driven"] + default: "Professional & Neutral" + required: true + language_level: + name: "Language Level" + description: "Language level to be used when communicating, relating to CEFR levels" + type: "enum" + allowed_values: ["Basic", "Standard", "Professional"] + default: "Standard" + required: true + welcome_message: + name: "Welcome Message" + description: "Introductory text given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language" + type: "text" + required: false + closing_message: + name: "Closing Message" + description: "Closing message given by the specialist - but translated according to Tone of Voice, Language Level and Starting Language" + type: "text" + required: false +competency_details: + title: + name: "Title" + description: "Competency Title" + type: "str" + required: true + description: + name: "Description" + description: "Description (in context of the role) of the competency" + type: "text" + required: true + is_knockout: + name: "KO" + description: "Defines if the competency is a knock-out criterium" + type: "boolean" + required: true + default: false + assess: + name: "Assess" + description: "Indication if this competency is to be assessed" + type: "boolean" + required: true + default: true +arguments: + region: + name: "Region" + type: "str" + description: "The region of the specific vacancy" + required: false + working_schedule: + name: "Work Schedule" + type: "str" + description: "The work schedule or employment type of the specific vacancy" + required: false + start_date: + name: "Start Date" + type: "date" + description: "The start date of the specific vacancy" + required: false + language: + name: "Language" + type: "str" + description: "The language (2-letter code) used to start the conversation" + required: true + interaction_mode: + name: "Interaction Mode" + type: "enum" + description: "The interaction mode the specialist will start working in." + allowed_values: ["Job Application", "Seduction"] + default: "Job Application" + required: true +results: + competencies: + name: "competencies" + type: "List[str, str]" + description: "List of vacancy competencies and their descriptions" + required: false +agents: + - type: "TRAICIE_HR_BP_AGENT" + version: "1.0" +tasks: + - type: "TRAICIE_GET_COMPETENCIES_TASK" + version: "1.1" +metadata: + author: "Josako" + date_added: "2025-05-27" + changes: "Add make to the selection specialist" + description: "Assistant to create a new Vacancy based on Vacancy Text" \ No newline at end of file diff --git a/config/type_defs/specialist_form_types.py b/config/type_defs/specialist_form_types.py index 57079e4..52de307 100644 --- a/config/type_defs/specialist_form_types.py +++ b/config/type_defs/specialist_form_types.py @@ -1,7 +1,11 @@ # Specialist Form Types SPECIALIST_FORM_TYPES = { "PERSONAL_CONTACT_FORM": { - "name": "Contact Form", + "name": "Personal Contact Form", "description": "A form for entering your personal contact details", }, + "PROFESSIONAL_CONTACT_FORM": { + "name": "Professional Contact Form", + "description": "A form for entering your professional contact details", + }, } \ No newline at end of file diff --git a/docker/update_chat_client_statics.sh b/docker/update_chat_client_statics.sh index ffa0c9f..a4af404 100755 --- a/docker/update_chat_client_statics.sh +++ b/docker/update_chat_client_statics.sh @@ -3,7 +3,7 @@ # Script to copy eveai_chat_client/static files to nginx/static # without overwriting existing files -SRC_DIR="../eveai_chat_client/static" +SRC_DIR="../eveai_chat_client/static/assets" DEST_DIR="../nginx/static/assets" # Check if source directory exists diff --git a/eveai_chat_client/static/css/chat-components.css b/eveai_chat_client/static/assets/css/chat-components.css similarity index 98% rename from eveai_chat_client/static/css/chat-components.css rename to eveai_chat_client/static/assets/css/chat-components.css index edb070c..e572293 100644 --- a/eveai_chat_client/static/css/chat-components.css +++ b/eveai_chat_client/static/assets/css/chat-components.css @@ -32,7 +32,6 @@ border-radius: 15px; background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); - border: 1px solid rgba(255,255,255,0.2); box-shadow: 0 4px 20px rgba(0,0,0,0.1); width: 100%; max-width: 1000px; /* Optimale breedte */ @@ -377,7 +376,7 @@ /* User message bubble styling */ .message.user .message-content { - background: linear-gradient(135deg, #007bff, #0056b3); + background: rgba(0, 0, 0, 0.1); color: white; border-bottom-right-radius: 4px; } @@ -385,9 +384,8 @@ /* AI/Bot message bubble styling */ .message.ai .message-content, .message.bot .message-content { - background: #f8f9fa; + background: rgba(255, 255, 255, 0.1); color: #212529; - border: 1px solid #e9ecef; border-bottom-left-radius: 4px; margin-right: 60px; } @@ -670,7 +668,6 @@ /* Progress Tracker Styling */ .progress-tracker { margin: 8px 0; - border: 1px solid #e9ecef; border-radius: 8px; background: #f8f9fa; overflow: hidden; @@ -683,8 +680,7 @@ } .progress-tracker.completed { - border-color: #28a745; - background: #d4edda; + background: rgba(155, 255, 155, 0.1); } .progress-header { diff --git a/eveai_chat_client/static/assets/css/chat-input.css b/eveai_chat_client/static/assets/css/chat-input.css new file mode 100644 index 0000000..7eb4b3b --- /dev/null +++ b/eveai_chat_client/static/assets/css/chat-input.css @@ -0,0 +1,120 @@ +/* ChatInput component styling */ + +/* Algemene container */ +.chat-input-container { + width: 100%; + padding: 10px; + background-color: #fff; + border-top: 1px solid #e0e0e0; + font-family: Arial, sans-serif; + font-size: 14px; +} + +/* Input veld en knoppen */ +.chat-input { + display: flex; + align-items: flex-end; + gap: 10px; +} + +.input-main { + flex: 1; + position: relative; +} + +.message-input { + width: 100%; + min-height: 40px; + padding: 10px 40px 10px 15px; + border: 1px solid #ddd; + border-radius: 20px; + resize: none; + outline: none; + transition: border-color 0.2s; + font-family: Arial, sans-serif; + font-size: 14px; +} + +.message-input:focus { + border-color: #0084ff; +} + +.message-input.over-limit { + border-color: #ff4d4f; +} + +/* Character counter */ +.character-counter { + position: absolute; + right: 10px; + bottom: 10px; + font-size: 12px; + color: #999; +} + +.character-counter.over-limit { + color: #ff4d4f; +} + +/* Input actions */ +.input-actions { + display: flex; + align-items: center; + gap: 8px; +} + +/* Verzendknop */ +.send-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: #0084ff; + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.2s; +} + +.send-btn:hover { + background-color: #0077e6; +} + +.send-btn:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +.send-btn.form-mode { + background-color: #4caf50; +} + +.send-btn.form-mode:hover { + background-color: #43a047; +} + +/* Loading spinner */ +.loading-spinner { + display: inline-block; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Formulier in chat input */ +.dynamic-form-container { + margin-bottom: 10px; + border: 1px solid #ddd; + border-radius: 8px; + padding: 15px 15px 5px 15px; + position: relative; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + font-family: Arial, sans-serif; + font-size: 14px; +} diff --git a/eveai_chat_client/static/assets/css/chat-message.css b/eveai_chat_client/static/assets/css/chat-message.css new file mode 100644 index 0000000..6302c93 --- /dev/null +++ b/eveai_chat_client/static/assets/css/chat-message.css @@ -0,0 +1,161 @@ +/* chat-message.css */ + +/* Algemene styling voor berichten */ +.message { + max-width: 90%; + margin-bottom: 15px; + width: auto; +} + +.message.user { + margin-left: auto; +} + +.message.ai { + margin-right: auto; +} + +.message-content { + width: 100%; + font-family: Arial, sans-serif; + font-size: 14px; +} + +/* Formulier styling */ +.form-display { + margin: 15px 0; + border-radius: 8px; + background-color: rgba(245, 245, 245, 0.7); + padding: 15px; + border: 1px solid #e0e0e0; + font-family: inherit; +} + +/* Tabel styling voor formulieren */ +.form-result-table { + width: 100%; + border-collapse: collapse; + font-family: inherit; +} + +.form-result-table th { + padding: 8px; + text-align: left; + border-bottom: 1px solid #e0e0e0; + font-weight: 600; + font-family: Arial, sans-serif; + font-size: 14px; +} + +.form-result-table td { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + font-family: Arial, sans-serif; + font-size: 14px; +} + +.form-result-table td:first-child { + font-weight: 500; + width: 35%; +} + +/* Styling voor formulier invoervelden */ +.form-result-table input.form-input, +.form-result-table textarea.form-textarea, +.form-result-table select.form-select { + width: 100%; + padding: 6px; + border-radius: 4px; + border: 1px solid #ddd; + font-family: Arial, sans-serif; + font-size: 14px; + background-color: white; +} + +.form-result-table textarea.form-textarea { + resize: vertical; + min-height: 60px; +} + +/* Styling voor tabel cellen */ +.form-result-table .field-label { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + font-weight: 500; + width: 35%; + vertical-align: top; +} + +.form-result-table .field-value { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + vertical-align: top; +} + +/* Toggle Switch styling */ +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.toggle-input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 24px; +} + +.toggle-knob { + position: absolute; + content: ''; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +/* Material icon styling */ +.material-symbols-outlined { + vertical-align: middle; + margin-right: 8px; + font-size: 20px; +} + +.form-header { + display: flex; + align-items: center; + padding: 8px; + border-bottom: 1px solid #e0e0e0; +} + +/* Zorgt dat het lettertype consistent is */ +.message-text { + font-family: Arial, sans-serif; + font-size: 14px; + white-space: pre-wrap; + word-break: break-word; +} + +/* Form error styling */ +.form-error { + color: red; + padding: 10px; + font-family: Arial, sans-serif; + font-size: 14px; +} diff --git a/eveai_chat_client/static/css/chat.css b/eveai_chat_client/static/assets/css/chat.css similarity index 100% rename from eveai_chat_client/static/css/chat.css rename to eveai_chat_client/static/assets/css/chat.css diff --git a/eveai_chat_client/static/assets/css/form-message.css b/eveai_chat_client/static/assets/css/form-message.css new file mode 100644 index 0000000..90220ba --- /dev/null +++ b/eveai_chat_client/static/assets/css/form-message.css @@ -0,0 +1,91 @@ +/* Styling voor formulier in berichten */ +.message .form-display { + margin-bottom: 12px; + border-radius: 8px; + background-color: rgba(245, 245, 245, 0.7); + padding: 12px; + border: 1px solid #e0e0e0; +} + +.message.user .form-display { + background-color: rgba(255, 255, 255, 0.1); +} + +.message.ai .form-display { + background-color: rgba(245, 245, 250, 0.7); +} +/* Styling voor formulieren in berichten */ + +.form-display { + margin-bottom: 10px; + border-radius: 8px; + padding: 12px; + background-color: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.user-form-values { + background-color: rgba(0, 123, 255, 0.05); +} + +/* Speciale styling voor read-only formulieren in user messages */ +.user-form .form-field { + margin-bottom: 6px !important; +} + +.user-form .field-label { + font-weight: 500 !important; + color: #555 !important; + padding: 2px 0 !important; +} + +.user-form .field-value { + padding: 2px 0 !important; +} + +/* Schakel hover effecten uit voor read-only formulieren */ +.read-only .form-field:hover { + background-color: transparent; +} + +/* Subtiele scheiding tussen velden */ +.dynamic-form.read-only .form-fields { + border-top: 1px solid rgba(0, 0, 0, 0.05); + margin-top: 10px; + padding-top: 8px; +} + +/* Verklein vorm titels in berichten */ +.message-form .form-title { + font-size: 1em !important; +} + +.message-form .form-description { + font-size: 0.85em !important; +} +.form-readonly { + width: 100%; +} + +.form-readonly .field-label { + font-weight: 500; + color: #555; +} + +.form-readonly .field-value { + word-break: break-word; +} + +.form-readonly .text-value { + white-space: pre-wrap; +} + +/* Algemene styling verbetering voor berichten */ +.message-text { + white-space: pre-wrap; + word-break: break-word; +} + +.message-content { + max-width: 100%; +} diff --git a/eveai_chat_client/static/css/form.css b/eveai_chat_client/static/assets/css/form.css similarity index 100% rename from eveai_chat_client/static/css/form.css rename to eveai_chat_client/static/assets/css/form.css diff --git a/eveai_chat_client/static/js/chat-app.js b/eveai_chat_client/static/assets/js/chat-app.js similarity index 88% rename from eveai_chat_client/static/js/chat-app.js rename to eveai_chat_client/static/assets/js/chat-app.js index d253aa3..94ad30b 100644 --- a/eveai_chat_client/static/js/chat-app.js +++ b/eveai_chat_client/static/assets/js/chat-app.js @@ -170,21 +170,22 @@ export const ChatApp = { }, // Message management - addMessage(content, sender, type = 'text', formData = null) { + addMessage(content, sender, type = 'text', formData = null, formValues = null) { const message = { id: this.messageIdCounter++, content, sender, type, formData, + formValues, timestamp: new Date().toISOString(), status: sender === 'user' ? 'sent' : 'delivered' }; this.allMessages.push(message); - // Initialize form values if it's a form - if (type === 'form' && formData) { + // Initialize form values if it's a form and no values were provided + if (type === 'form' && formData && !formValues) { // Vue 3 compatibele manier om reactieve objecten bij te werken this.formValues[message.id] = {}; formData.fields.forEach(field => { @@ -203,19 +204,32 @@ export const ChatApp = { return message; }, + // Helper functie om formulierdata toe te voegen aan bestaande berichten + attachFormDataToMessage(messageId, formData, formValues) { + const message = this.allMessages.find(m => m.id === messageId); + if (message) { + message.formData = formData; + message.formValues = formValues; + } + }, + updateCurrentMessage(value) { this.currentMessage = value; }, - // Message sending + // Message sending (alleen voor gewone tekstberichten, geen formulieren) async sendMessage() { const text = this.currentMessage.trim(); + + // Controleer of we kunnen verzenden if (!text || this.isLoading) return; - console.log('Sending message:', text); + console.log('Sending text message:', text); // Add user message const userMessage = this.addMessage(text, 'user', 'text'); + + // Wis input this.currentMessage = ''; // Show typing and loading state @@ -223,11 +237,14 @@ export const ChatApp = { this.isLoading = true; try { - const response = await this.callAPI('/api/send_message', { + // Verzamel gegevens voor de API call + const apiData = { message: text, conversation_id: this.conversationId, user_id: this.userId - }); + }; + + const response = await this.callAPI('/api/send_message', apiData); // Hide typing indicator this.isTyping = false; @@ -278,35 +295,61 @@ export const ChatApp = { return; } - console.log('Submitting form from input:', this.currentInputFormData.title, formValues); + console.log('Form values received:', formValues); + console.log('Current input form data:', this.currentInputFormData); try { - const response = await this.callAPI('/api/submit_form', { - formData: formValues, - formType: this.currentInputFormData.title, + // Maak een user message met formuliergegevens én eventuele tekst + const userMessage = this.addMessage( + this.currentMessage.trim(), // Voeg tekst toe als die er is + 'user', + 'text' + ); + + // Voeg formuliergegevens toe aan het bericht + userMessage.formData = this.currentInputFormData; + userMessage.formValues = formValues; + + // Reset het tekstbericht + this.currentMessage = ''; + this.$emit('update-message', ''); + + // Toon laad-indicator + this.isTyping = true; + this.isLoading = true; + + // Verzamel gegevens voor de API call + const apiData = { + message: userMessage.content, conversation_id: this.conversationId, - user_id: this.userId - }); + user_id: this.userId, + form_values: formValues // Voeg formuliergegevens toe aan API call + }; - if (response.success) { - this.addMessage( - `✅ ${response.message || 'Formulier succesvol verzonden!'}`, - 'ai', - 'text' - ); + // Verstuur bericht naar de API + const response = await this.callAPI('/api/send_message', apiData); - // Wis het huidige formulier (ongeacht of het succesvol was of niet) - this.currentInputFormData = null; - } else { - this.addMessage( - `❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`, - 'ai', - 'text' - ); - // Wis ook hier het formulier na een fout - this.currentInputFormData = null; + // Verberg de typing indicator + this.isTyping = false; + + // Markeer het gebruikersbericht als afgeleverd + userMessage.status = 'delivered'; + + // Voeg AI response toe met task_id voor tracking + const aiMessage = this.addMessage( + '', + 'ai', + 'text' + ); + + if (response.task_id) { + console.log('Monitoring Task ID: ', response.task_id); + aiMessage.taskId = response.task_id; } + // Reset formulier na succesvolle verzending + this.currentInputFormData = null; + } catch (error) { console.error('Error submitting form:', error); this.addMessage( @@ -318,6 +361,7 @@ export const ChatApp = { this.currentInputFormData = null; } finally { this.isSubmittingForm = false; + this.isLoading = false; } }, diff --git a/eveai_chat_client/static/js/components/ChatInput.js b/eveai_chat_client/static/assets/js/components/ChatInput.js similarity index 92% rename from eveai_chat_client/static/js/components/ChatInput.js rename to eveai_chat_client/static/assets/js/components/ChatInput.js index d87f60c..705181a 100644 --- a/eveai_chat_client/static/js/components/ChatInput.js +++ b/eveai_chat_client/static/assets/js/components/ChatInput.js @@ -125,9 +125,15 @@ 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...'; @@ -181,20 +187,31 @@ 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 + // Verstuur het formulier, eventueel met aanvullende tekst this.$emit('submit-form', this.formValues); - this.formValues = {}; } } else if (this.localMessage.trim()) { - // Verstuur normaal bericht + // Verstuur normaal bericht zonder formulier this.$emit('send-message'); } }, - // Deze methode houden we voor de volledigheid, maar zal niet meer direct worden aangeroepen + 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 @@ -261,7 +278,7 @@ -
+
Fout: Geen velden gevonden in formulier
{ + // Formulier stylesheet + if (!document.querySelector('link[href*="form-message.css"]')) { + const formLink = document.createElement('link'); + formLink.rel = 'stylesheet'; + formLink.href = '/static/assets/css/form-message.css'; + document.head.appendChild(formLink); + } + + // Chat bericht stylesheet + if (!document.querySelector('link[href*="chat-message.css"]')) { + const chatLink = document.createElement('link'); + chatLink.rel = 'stylesheet'; + chatLink.href = '/static/assets/css/chat-message.css'; + document.head.appendChild(chatLink); + } + + // Material Icons font stylesheet + if (!document.querySelector('link[href*="Material+Symbols+Outlined"]')) { + const iconLink = document.createElement('link'); + iconLink.rel = 'stylesheet'; + iconLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0'; + document.head.appendChild(iconLink); + } +}; + +// Laad de stylesheets +addStylesheets(); + export const ChatMessage = { name: 'ChatMessage', props: { @@ -17,11 +47,38 @@ export const ChatMessage = { 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); + } + }, + 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 }; }, + 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: { handleSpecialistError(eventData) { console.log('ChatMessage received specialist-error event:', eventData); @@ -90,7 +147,7 @@ export const ChatMessage = {
diff --git a/eveai_chat_client/static/js/components/FormField.js b/eveai_chat_client/static/assets/js/components/FormField.js similarity index 100% rename from eveai_chat_client/static/js/components/FormField.js rename to eveai_chat_client/static/assets/js/components/FormField.js diff --git a/eveai_chat_client/static/js/components/FormMessage.js b/eveai_chat_client/static/assets/js/components/FormMessage.js similarity index 100% rename from eveai_chat_client/static/js/components/FormMessage.js rename to eveai_chat_client/static/assets/js/components/FormMessage.js diff --git a/eveai_chat_client/static/js/components/MaterialIconManager.js b/eveai_chat_client/static/assets/js/components/MaterialIconManager.js similarity index 100% rename from eveai_chat_client/static/js/components/MaterialIconManager.js rename to eveai_chat_client/static/assets/js/components/MaterialIconManager.js diff --git a/eveai_chat_client/static/js/components/MessageHistory.js b/eveai_chat_client/static/assets/js/components/MessageHistory.js similarity index 100% rename from eveai_chat_client/static/js/components/MessageHistory.js rename to eveai_chat_client/static/assets/js/components/MessageHistory.js diff --git a/eveai_chat_client/static/js/components/ProgressTracker.js b/eveai_chat_client/static/assets/js/components/ProgressTracker.js similarity index 100% rename from eveai_chat_client/static/js/components/ProgressTracker.js rename to eveai_chat_client/static/assets/js/components/ProgressTracker.js diff --git a/eveai_chat_client/static/js/components/TypingIndicator.js b/eveai_chat_client/static/assets/js/components/TypingIndicator.js similarity index 100% rename from eveai_chat_client/static/js/components/TypingIndicator.js rename to eveai_chat_client/static/assets/js/components/TypingIndicator.js diff --git a/eveai_chat_client/static/js/iconManager.js b/eveai_chat_client/static/assets/js/iconManager.js similarity index 100% rename from eveai_chat_client/static/js/iconManager.js rename to eveai_chat_client/static/assets/js/iconManager.js diff --git a/eveai_chat_client/static/css/chat-input.css b/eveai_chat_client/static/css/chat-input.css deleted file mode 100644 index 282e5c3..0000000 --- a/eveai_chat_client/static/css/chat-input.css +++ /dev/null @@ -1,11 +0,0 @@ -/* Animation styles for ChatInput component */ -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Add more ChatInput-specific styles here */ -.loading-spinner { - display: inline-block; - animation: spin 1s linear infinite; -} diff --git a/eveai_chat_client/static/css/form-message.css b/eveai_chat_client/static/css/form-message.css deleted file mode 100644 index 6b3b636..0000000 --- a/eveai_chat_client/static/css/form-message.css +++ /dev/null @@ -1,75 +0,0 @@ -/* 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/views/chat_views.py b/eveai_chat_client/views/chat_views.py index 0d55c59..2c7ac25 100644 --- a/eveai_chat_client/views/chat_views.py +++ b/eveai_chat_client/views/chat_views.py @@ -117,10 +117,12 @@ def send_message(): """ try: data = request.json - message = data.get('message') + message = data.get('message', '') + form_values = data.get('form_values', {}) - if not message: - return jsonify({'error': 'No message provided'}), 400 + # Controleer of er ofwel een bericht of formuliergegevens zijn + if not message and not form_values: + return jsonify({'error': 'No message or form data provided'}), 400 tenant_id = session['tenant']['id'] specialist_id = session['specialist']['id'] @@ -134,7 +136,12 @@ def send_message(): Database(tenant_id).switch_schema() # Add user message to specialist arguments - specialist_args['question'] = message + if message: + specialist_args['question'] = message + + # Add form values to specialist arguments if present + if form_values: + specialist_args['form_values'] = form_values current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n" f" with args: {specialist_args}\n" diff --git a/eveai_chat_workers/outputs/traicie/competencies/competencies_v1_1.py b/eveai_chat_workers/outputs/traicie/competencies/competencies_v1_1.py index b8a5fe7..6e261a8 100644 --- a/eveai_chat_workers/outputs/traicie/competencies/competencies_v1_1.py +++ b/eveai_chat_workers/outputs/traicie/competencies/competencies_v1_1.py @@ -2,6 +2,7 @@ from typing import List, Optional from pydantic import BaseModel, Field from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem + # class BehaviouralCompetence(BaseModel): # title: str = Field(..., description="The title of the behavioural competence.") # description: Optional[str] = Field(None, description="The description of the behavioural competence.") diff --git a/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_2.py b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_2.py new file mode 100644 index 0000000..856c2f8 --- /dev/null +++ b/eveai_chat_workers/specialists/traicie/TRAICIE_SELECTION_SPECIALIST/1_2.py @@ -0,0 +1,254 @@ +import asyncio +import json +from os import wait +from typing import Optional, List, Dict, Any +from datetime import date +from time import sleep +from crewai.flow.flow import start, listen, and_ +from flask import current_app +from pydantic import BaseModel, Field, EmailStr +from sqlalchemy.exc import SQLAlchemyError + +from common.extensions import db +from common.models.user import Tenant +from common.models.interaction import Specialist +from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem +from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor +from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments +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): + """ + type: TRAICIE_SELECTION_SPECIALIST + type_version: 1.1 + Traicie Selection Specialist Executor class + """ + + def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs): + self.role_definition_crew = None + + super().__init__(tenant_id, specialist_id, session_id, task_id) + + # Load the Tenant & set language + self.tenant = Tenant.query.get_or_404(tenant_id) + + @property + def type(self) -> str: + return "TRAICIE_SELECTION_SPECIALIST" + + @property + def type_version(self) -> str: + return "1.1" + + def _config_task_agents(self): + self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent") + + def _config_pydantic_outputs(self): + self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies") + + def _instantiate_specialist(self): + verbose = self.tuning + + role_definition_agents = [self.traicie_hr_bp_agent] + role_definition_tasks = [self.traicie_get_competencies_task] + self.role_definition_crew = EveAICrewAICrew( + self, + "Role Definition Crew", + agents=role_definition_agents, + tasks=role_definition_tasks, + verbose=verbose, + ) + + self.flow = RoleDefinitionFlow( + self, + self.role_definition_crew + ) + + def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult: + self.log_tuning("Traicie Selection Specialist execution started", {}) + + # flow_inputs = { + # "vacancy_text": arguments.vacancy_text, + # "role_name": arguments.role_name, + # 'role_reference': arguments.role_reference, + # } + # + # flow_results = self.flow.kickoff(inputs=flow_inputs) + # + # flow_state = self.flow.state + # + # results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version) + # if flow_state.competencies: + # results.competencies = flow_state.competencies + + # self.create_selection_specialist(arguments, flow_state.competencies) + 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}", + form_request=contact_form) + + self.log_tuning(f"Traicie Selection Specialist execution ended", {"Results": results.model_dump()}) + + return results + + def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]): + """This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies.""" + current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}") + selection_comptencies = [] + for competency in competencies: + selection_competency = { + "title": competency.title, + "description": competency.description, + "assess": True, + "is_knockout": False, + } + selection_comptencies.append(selection_competency) + + selection_config = { + "name": arguments.specialist_name, + "competencies": selection_comptencies, + "tone_of_voice": "Professional & Neutral", + "language_level": "Standard", + "role_reference": arguments.role_reference, + } + name = arguments.role_name + if len(name) > 50: + name = name[:47] + "..." + + new_specialist = Specialist( + name=name, + description=f"Specialist for {arguments.role_name} role", + type="TRAICIE_SELECTION_SPECIALIST", + type_version="1.0", + tuning=False, + configuration=selection_config, + ) + try: + db.session.add(new_specialist) + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + current_app.logger.error(f"Error creating selection specialist: {str(e)}") + raise e + + SpecialistServices.initialize_specialist(new_specialist.id, self.type, self.type_version) + + +class SelectionSpecialistInput(BaseModel): + region: str = Field(..., alias="region") + working_schedule: Optional[str] = Field(..., alias="working_schedule") + start_date: Optional[date] = Field(None, alias="vacancy_text") + language: Optional[str] = Field(None, alias="language") + interaction_mode: Optional[str] = Field(None, alias="interaction_mode") + question: Optional[str] = Field(None, alias="question") + field_values: Optional[Dict[str, Any]] = Field(None, alias="field_values") + + +class SelectionSpecialistKOCriteriumScore(BaseModel): + criterium: Optional[str] = Field(None, alias="criterium") + answer: Optional[str] = Field(None, alias="answer") + score: Optional[int] = Field(None, alias="score") + + +class SelectionSpecialistCompetencyScore(BaseModel): + competency: Optional[str] = Field(None, alias="competency") + answer: Optional[str] = Field(None, alias="answer") + score: Optional[int] = Field(None, alias="score") + + +class PersonalContactData(BaseModel): + name: str = Field(..., description="Your name", alias="name") + email: EmailStr = Field(..., description="Your Name", alias="email") + phone: str = Field(..., description="Your Phone Number", alias="phone") + address: Optional[str] = Field(None, description="Your Address", alias="address") + zip: Optional[str] = Field(None, description="Postal Code", alias="zip") + city: Optional[str] = Field(None, description="City", alias="city") + country: Optional[str] = Field(None, description="Country", alias="country") + consent: bool = Field(..., description="Consent", alias="consent") + + +class SelectionSpecialistResult(SpecialistResult): + ko_criteria_scores: Optional[List[SelectionSpecialistKOCriteriumScore]] = Field( + None, alias="ko_criteria_scores" + ) + competency_scores: Optional[List[SelectionSpecialistCompetencyScore]] = Field( + None, alias="competency_scores" + ) + personal_contact_data: Optional[PersonalContactData] = Field( + None, alias="personal_contact_data" + ) + + +class SelectionSpecialistFlowState(EveAIFlowState): + """Flow state for Traicie Role Definition specialist that automatically updates from task outputs""" + input: Optional[SelectionSpecialistInput] = None + ko_criteria_scores: Optional[List[SelectionSpecialistKOCriteriumScore]] = Field( + None, alias="ko_criteria_scores" + ) + competency_scores: Optional[List[SelectionSpecialistCompetencyScore]] = Field( + None, alias="competency_scores" + ) + personal_contact_data: Optional[PersonalContactData] = Field( + None, alias="personal_contact_data" + ) + phase: Optional[str] = Field(None, alias="phase") + interaction_mode: Optional[str] = Field(None, alias="mode") + + +class RoleDefinitionFlow(EveAICrewAIFlow[SelectionSpecialistFlowState]): + def __init__(self, + specialist_executor: CrewAIBaseSpecialistExecutor, + role_definition_crew: EveAICrewAICrew, + **kwargs): + super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs) + self.specialist_executor = specialist_executor + self.role_definition_crew = role_definition_crew + self.exception_raised = False + + @start() + def process_inputs(self): + return "" + + @listen(process_inputs) + async def execute_role_definition (self): + inputs = self.state.input.model_dump() + try: + current_app.logger.debug("In execute_role_definition") + crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs) + # Unfortunately, crew_output will only contain the output of the latest task. + # As we will only take into account the flow state, we need to ensure both competencies and criteria + # are copies to the flow state. + update = {} + for task in self.role_definition_crew.tasks: + current_app.logger.debug(f"Task {task.name} output:\n{task.output}") + if task.name == "traicie_get_competencies_task": + # update["competencies"] = task.output.pydantic.competencies + self.state.competencies = task.output.pydantic.competencies + # crew_output.pydantic = crew_output.pydantic.model_copy(update=update) + current_app.logger.debug(f"State after execute_role_definition: {self.state}") + current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}") + return crew_output + except Exception as e: + current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}") + self.exception_raised = True + raise e + + async def kickoff_async(self, inputs=None): + current_app.logger.debug(f"Async kickoff {self.name}") + current_app.logger.debug(f"Inputs: {inputs}") + self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs) + current_app.logger.debug(f"State: {self.state}") + result = await super().kickoff_async(inputs) + return self.state