// Adaptive Form for Hems Version X // Helper function to get URL parameters (add this outside the class) function getUrlParameter(name) { name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); const regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); const results = regex.exec(location.search); return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); } const TextAnimator = (function() { function createStyle() { if (!document.getElementById('text-animator-style')) { const style = document.createElement('style'); style.id = 'text-animator-style'; style.textContent = ` @keyframes quickFadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } .text-animator-wrapper { display: inline; white-space: normal; } .text-animator-word { display: inline; opacity: 0; } `; document.head.appendChild(style); } } createStyle(); return { animate: function(element, text, options = {}) { const defaults = { wordDelay: 100, animationDuration: 0.5, commaPauseDuration: 320, // Duration of pause after comma sentencePauseDuration: 520 // Duration of pause after period, question mark, or exclamation point }; const settings = { ...defaults, ...options }; if (!element) return Promise.resolve(); // Split text into words const words = text.split(' '); // Clear the element element.innerHTML = ''; // Create a wrapper to hold the words const wrapper = document.createElement('span'); wrapper.className = 'text-animator-wrapper'; let totalDelay = 0; let lastWordStartTime = 0; // Track when the last word actually starts words.forEach((word, index) => { // Create and append word span const wordSpan = document.createElement('span'); wordSpan.textContent = index < words.length - 1 ? word + ' ' : word; wordSpan.className = 'text-animator-word'; // Check if the word ends with different types of punctuation const endsWithComma = /,$/.test(word); const endsWithSentence = /[.!?]$/.test(word); wordSpan.style.animation = `quickFadeIn ${settings.animationDuration}s ease-out ${totalDelay / 1000}s forwards`; wrapper.appendChild(wordSpan); // Store the start time of this word (which will be the last one after the loop) lastWordStartTime = totalDelay; // Increase total delay for the NEXT word totalDelay += settings.wordDelay; // Add extra pause after punctuation if (endsWithComma) { totalDelay += settings.commaPauseDuration; } else if (endsWithSentence) { totalDelay += settings.sentencePauseDuration; } }); element.appendChild(wrapper); // FIXED: Calculate when the last word actually finishes const totalAnimationTime = lastWordStartTime + (settings.animationDuration * 1000); return new Promise(resolve => { setTimeout(resolve, totalAnimationTime); }); }, // Updated method to handle frame transitions handleFrameTransition: function(element, text, options = {}) { const wrapper = element.querySelector('.text-animator-wrapper'); if (wrapper && wrapper.textContent.trim() === text.trim()) { // If the text is the same, just reset the animation const words = wrapper.querySelectorAll('.text-animator-word'); let totalDelay = 0; words.forEach((word, index) => { word.style.animation = 'none'; word.offsetHeight; // Trigger reflow const endsWithComma = /,$/.test(word.textContent); const endsWithSentence = /[.!?]$/.test(word.textContent); word.style.animation = `quickFadeIn ${options.animationDuration || 0.2}s ease-out ${totalDelay / 1000}s forwards`; totalDelay += (options.wordDelay || 100); if (endsWithComma) { totalDelay += (options.commaPauseDuration || 200); } else if (endsWithSentence) { totalDelay += (options.sentencePauseDuration || 500); } }); // Return promise for consistency const totalAnimationTime = totalDelay + ((options.animationDuration || 0.2) * 1000); return new Promise(resolve => { setTimeout(resolve, totalAnimationTime); }); } else { // If the text is different, use the regular animate method return this.animate(element, text, options); } } }; })(); const SESSION_EXPIRATION_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds // Month names and variations for text-based month entry const germanMonths = { // Standard names 'januar': 1, 'februar': 2, 'märz': 3, 'april': 4, 'mai': 5, 'juni': 6, 'juli': 7, 'august': 8, 'september': 9, 'oktober': 10, 'november': 11, 'dezember': 12, // Common variations and misspellings 'jan': 1, 'feb': 2, 'mär': 3, 'mrz': 3, 'apr': 4, 'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'sept': 9, 'okt': 10, 'oct': 10, 'nov': 11, 'dez': 12, 'dec': 12, // English month names for international support 'january': 1, 'february': 2, 'march': 3, 'april': 4, 'may': 5, 'june': 6, 'july': 7, 'august': 8, 'september': 9, 'october': 10, 'november': 11, 'december': 12 }; // Full month names for output const fullMonthNames = { 'en': ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 'de': ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'] }; // Function to check if a year is a leap year function isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); } // Enhanced date validation functions function isValidDay(day, month, year) { if (day < 1 || day > 31) return false; // Check days in month (account for leap years) const daysInMonth = [31, (isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if (month > 0 && month <= 12) { return day <= daysInMonth[month - 1]; } return true; // If month is not valid yet, just do basic validation } function isValidMonth(month) { return month >= 1 && month <= 12; } // Function to find the closest matching month name function findClosestMonth(input) { input = input.toLowerCase().trim(); // Direct match in our dictionary if (germanMonths[input] !== undefined) { return germanMonths[input]; } // Try to match as a number const numValue = parseInt(input, 10); if (!isNaN(numValue) && numValue >= 1 && numValue <= 12) { return numValue; } // If input is very short (1-2 chars), don't try fuzzy matching if (input.length < 3) { return null; } // Simple fuzzy matching for misspellings // Look for the entry with most character matches let bestMatch = null; let highestMatchCount = 0; for (const month in germanMonths) { if (month.length < 3) continue; // Skip abbreviated months for fuzzy matching let matchCount = 0; for (let i = 0; i < input.length && i < month.length; i++) { if (input[i] === month[i]) { matchCount++; } } // Weight by the length difference to prefer closer length matches const lengthDiff = Math.abs(month.length - input.length); const weightedMatch = matchCount - (lengthDiff * 0.5); if (weightedMatch > highestMatchCount) { highestMatchCount = weightedMatch; bestMatch = month; } } // Only return a match if it's reasonably close if (bestMatch && highestMatchCount > Math.min(3, input.length * 0.6)) { return germanMonths[bestMatch]; } return null; } class ChatBasedFormController { constructor() { // Core elements this.container = document.getElementById('contform'); this.nextButton = document.getElementById('nextcta'); this.backButton = document.getElementById('backcta'); // Add session expiration time as an instance property this.SESSION_EXPIRATION_TIME = SESSION_EXPIRATION_TIME; this.sessionId = null; this.chatHistory = this.loadChatHistoryFromStorage() || []; this.contextToken = null; // Animation & transition control this.isAnimating = false; this.currentModule = null; this.autoAdvanceTimer = null; // Bind event handlers this.handleNext = this.handleNext.bind(this); this.handleBack = this.handleBack.bind(this); // Add properties for delayed loading this.loadingTimer = null; this.loadingDelay = 800; // Delay in ms before showing loading animation this.knownSlowOperations = ['Initialisiere...', 'Lade Ihre Daten...']; // API endpoint this.apiBaseUrl = 'https://api.hems-app.com/api:fUxdXSnV/en/homesearch'; this.nextEndpoint = '/next'; this.authState = { email: null, authMethod: null, isAuthenticated: false }; } // Decode a message ID back to a step ID decodeMessageId(messageId) { if (!messageId || !messageId.startsWith('msg_')) return null; const parts = messageId.split('_'); if (parts.length < 4) return null; try { return atob(parts[3]); } catch (e) { console.error('Failed to decode message ID', e); return null; } } persistChatHistoryToStorage() { try { localStorage.setItem('chatHistory', JSON.stringify(this.chatHistory)); localStorage.setItem('sessionId', this.sessionId); localStorage.setItem('sessionTimestamp', Date.now().toString()); if (this.contextToken) { localStorage.setItem('contextToken', this.contextToken); } } catch (error) { console.warn('Failed to persist chat history to localStorage:', error); } } loadChatHistoryFromStorage() { try { const savedSessionId = localStorage.getItem('sessionId'); const savedContextToken = localStorage.getItem('contextToken'); const savedHistory = localStorage.getItem('chatHistory'); const sessionTimestamp = localStorage.getItem('sessionTimestamp'); console.log("Checking saved session:", { hasSessionId: !!savedSessionId, hasContextToken: !!savedContextToken, hasHistory: !!savedHistory, hasTimestamp: !!sessionTimestamp }); // Check for session expiration if (sessionTimestamp) { const currentTime = Date.now(); const timestamp = parseInt(sessionTimestamp, 10); const sessionAge = currentTime - timestamp; console.log(`Session age: ${Math.round(sessionAge / 1000 / 60)} minutes`); if (sessionAge > this.SESSION_EXPIRATION_TIME) { console.log("Session expired, creating new session"); // Session expired, clear localStorage and return null localStorage.removeItem('chatHistory'); localStorage.removeItem('sessionId'); localStorage.removeItem('contextToken'); localStorage.removeItem('sessionTimestamp'); return null; } } if (savedSessionId) { this.sessionId = savedSessionId; } if (savedContextToken) { this.contextToken = savedContextToken; } if (savedHistory) { return JSON.parse(savedHistory); } return null; } catch (error) { console.warn('Failed to load chat history from localStorage:', error); return null; } } // Initialize the chat-based form async initialize() { try { // Show loading indicator for initialization // this.showLoadingIndicator('Initialisiere...'); // Check if we have a valid local session const storedHistory = this.loadChatHistoryFromStorage(); if (!this.sessionId || !storedHistory || storedHistory.length === 0) { // No valid session, create a new one const sessionData = await this.createSession(); this.sessionId = sessionData.session_id; localStorage.setItem('sessionId', this.sessionId); localStorage.setItem('sessionTimestamp', Date.now().toString()); this.chatHistory = []; await this.startNewSession(); } else { // We have a valid session and history, continue from where we left off this.chatHistory = storedHistory; // Update timestamp on successful continuation localStorage.setItem('sessionTimestamp', Date.now().toString()); // Get the last message from history const lastMessage = this.chatHistory[this.chatHistory.length - 1]; // Display all messages from history await this.rebuildUIFromHistory(); //if (lastMessage) { // try { // const nextStepData = await this.requestNextStep(lastMessage.message_id, false); // if (nextStepData) { // // Process the next step normally // this.processNextStep(nextStepData); // } // } catch (error) { // console.error("Failed to continue session, creating new one:", error); // // Create a new session if continuation fails // await this.resetAndStartNewSession(); // } //} else { // // Something wrong with history, start new // await this.resetAndStartNewSession(); //} } // Set up event listeners this.nextButton.addEventListener('click', this.handleNext); this.backButton.addEventListener('click', this.handleBack); // Add ENTER key support for desktop users document.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !this.isAnimating) { const isInactive = this.nextButton.classList.contains('inactive'); if (!isInactive) { this.handleNext(); } } }); // Inject required styles this.injectStyles(); } catch (error) { console.error("Failed to initialize form:", error); // Create new session silently instead of showing error await this.resetAndStartNewSession(); } } setupRefreshPrevention() { window.addEventListener('beforeunload', (event) => { // Only show warning if user has started the conversation if (this.chatHistory.length > 1) { // Standard way to show a confirmation dialog before leaving const message = 'Wenn Sie die Seite verlassen, gehen Ihre Eingaben verloren. Möchten Sie wirklich fortfahren?'; event.returnValue = message; return message; } }); } async createSession() { try { // Get apptoken from URL parameters if it exists const apptoken = getUrlParameter('apptoken'); // Get city parameter from URL const cityIntent = getUrlParameter('location'); // Create form data const formData = new FormData(); if (apptoken) { formData.append('apptoken', apptoken); console.log("Found apptoken in URL, adding to request:", apptoken); } // Add city parameter if it exists if (cityIntent) { formData.append('location', cityIntent); console.log("Found city parameter in URL:", cityIntent); } const response = await fetch(`https://api.hems-app.com/api:fUxdXSnV/homesearch/create_session`, { method: 'POST', mode: 'cors', credentials: 'include', body: formData }); if (!response.ok) { if (response.status === 429) { throw new Error('Rate limit exceeded. Please try again later.'); } throw new Error(`Failed to create session: ${response.statusText}`); } const sessionData = await response.json(); // Store contextToken if it's provided if (sessionData.contextToken) { this.contextToken = sessionData.contextToken; // The contextToken now contains the city_intent information // We don't need to separately store it } return sessionData; } catch (error) { console.error("Session creation failed:", error); throw error; } } // Add this method to your class debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Handle resume prompt for existing session async handleResumePrompt(resumableSession) { return new Promise((resolve) => { // Create a resume prompt UI const resumePrompt = document.createElement('div'); resumePrompt.className = 'form-module resume-prompt'; resumePrompt.innerHTML = `

Gespräch fortsetzen?

Wir haben ein unvollständiges Gespräch von Ihnen gefunden. Möchten Sie fortfahren, wo Sie aufgehört haben?

${resumableSession.progress || 0}% abgeschlossen
`; // Add to container this.container.innerHTML = ''; this.container.appendChild(resumePrompt); this.currentModule = resumePrompt; // Add button handlers const resumeYesButton = resumePrompt.querySelector('.resume-yes'); const resumeNoButton = resumePrompt.querySelector('.resume-no'); resumeYesButton.addEventListener('click', async () => { // Resume existing session this.showLoadingIndicator('Lade Ihre Daten...'); await this.resumeSession(); resolve(); }); resumeNoButton.addEventListener('click', async () => { // Start a new session this.showLoadingIndicator('Starte neues Gespräch...'); // Reset the session await this.resetSession(); // Start new session setTimeout(() => { this.startNewSession(); resolve(); }, 1000); }); }); } // Reset a session async resetSession() { try { const response = await fetch(`${this.apiBaseUrl}/reset_session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId }) }); if (!response.ok) { throw new Error(`Failed to reset session: ${response.statusText}`); } // Clear local state this.chatHistory = []; this.currentContext = {}; // Persist empty chat history this.persistChatHistoryToStorage(); } catch (error) { console.error("Failed to reset session:", error); this.showErrorMessage("Beim Zurücksetzen ist ein Fehler aufgetreten."); throw error; } } // Start a new session async startNewSession() { try { // this.showLoadingIndicator('Starte Gespräch...'); // For starting a form, directly get the first step/message without starthomesearch action const response = await fetch(`${this.apiBaseUrl}${this.nextEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId, chatHistory: this.chatHistory, contextToken: this.contextToken }) }); if (!response.ok) { throw new Error(`Failed to start session: ${response.statusText}`); } const data = await response.json(); if (data.action === "next_step" && data.payload) { // Extract step data from the payload - might be nested inside 'step' const stepData = data.payload.step || data.payload; // Make sure type is defined if (!stepData.type) { console.warn("Step type is undefined:", stepData); // Set a default type to prevent errors stepData.type = 'message-only'; } // Convert step to message format and process this.processNextStep(stepData); } else if (data.action === "error" && data.payload) { this.handleError(data.payload); } else { console.warn("Received unexpected response format:", data); this.showErrorMessage("Unerwartete Antwort vom Server erhalten."); } } catch (error) { console.error("Failed to start new session:", error); this.showErrorMessage("Beim Starten ist ein Fehler aufgetreten."); } } async sendUserInput(messageId, answer) { this.showLoadingIndicator('Verarbeite...'); try { const messageIndex = this.chatHistory.findIndex(msg => msg.message_id === messageId); // Create a variable for transformed answer outside the if block let apiAnswer = answer; let currentMessage = null; if (messageIndex >= 0) { // Update the input in the chat history this.chatHistory[messageIndex].input = answer; // Persist updated chat history this.persistChatHistoryToStorage(); currentMessage = this.chatHistory[messageIndex]; // Transform array answers to comma-separated strings for the API if (currentMessage.type === 'checkbox-list' && Array.isArray(answer)) { console.log("Converting checkbox array to string:", answer); apiAnswer = answer.join(','); } // Check if this is an auth step - but keep this inside the if block if (currentMessage && currentMessage.isAuthStep) { await this.handleAuthStep(currentMessage, answer); // Use original answer for auth return; } } // Normal message handling continues outside the if block this.showLoadingIndicator('Verarbeite...'); // Get next message/step using the transformed answer const response = await fetch(`${this.apiBaseUrl}${this.nextEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId, messageId: messageId, answer: apiAnswer, // Use the potentially transformed answer chatHistory: this.chatHistory, contextToken: this.contextToken }) }); if (!response.ok) { throw new Error(`Failed to send input: ${response.statusText}`); } const data = await response.json(); if (data.action === "next_step" && data.payload) { this.processNextStep(data.payload); } else if (data.action === "form_complete" && data.payload) { this.handleFormCompletion(data.payload); } else if (data.action === "error" && data.payload) { this.handleError(data.payload); } else if (data.action === "session_expired") { this.handleSessionExpired(); } else { console.warn("Received unexpected response format:", data); this.showErrorMessage("Unerwartete Antwort vom Server erhalten."); } } catch (error) { console.error("Failed to send input:", error); this.showErrorMessage("Beim Senden Ihrer Antwort ist ein Fehler aufgetreten."); } } // Request next message/step with no input (for responder steps) async requestNextStep(currentMessageId, showLoading = true) { // Only show loading indicator if showLoading is true if (showLoading) { this.showLoadingIndicator('Laden...'); } try { const response = await fetch(`${this.apiBaseUrl}${this.nextEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId, messageId: currentMessageId, chatHistory: this.chatHistory, contextToken: this.contextToken }) }); if (!response.ok) { throw new Error(`Failed to request next step: ${response.statusText}`); } const data = await response.json(); if (data.action === "next_step" && data.payload) { return data.payload; } else if (data.action === "form_complete" && data.payload) { this.handleFormCompletion(data.payload); return null; } else if (data.action === "error" && data.payload) { this.handleError(data.payload); return null; } else if (data.action === "session_expired") { this.handleSessionExpired(); return null; } else { console.warn("Received unexpected response format:", data); this.showErrorMessage("Unerwartete Antwort vom Server erhalten."); return null; } } catch (error) { console.error("Failed to request next step:", error); if (showLoading) { // Only show error if we were showing loading this.showErrorMessage("Beim Laden des nächsten Schritts ist ein Fehler aufgetreten."); } return null; } } async processNextStep(stepData) { // Cancel any pending loading timers this.cancelLoadingTimer(); // Clean up any existing slide-up inputs this.cleanupSlideUpInputs(); // Create message object with additional properties const message = { message_id: stepData.message_id, title: stepData.title || '', message: stepData.message || stepData.title || '', type: stepData.type || 'message-only', // Default type to prevent errors options: stepData.options || [], input: null, // Will be filled when user provides input required: stepData.required || false, autoAdvance: stepData.autoAdvance || false, autoAdvanceOnSelect: stepData.autoAdvanceOnSelect || false, autoAdvanceOnChange: stepData.autoAdvanceOnChange || false, displayTime: stepData.displayTime || 3000, responderText: stepData.responderText || null, explainer: stepData.explainer || null, minDate: stepData.minDate || null, maxDate: stepData.maxDate || null, label: stepData.label || null, validate: stepData.validate || null, // Add placeholder from step data placeholder: stepData.placeholder || null, // Add auth-related properties if they exist isAuthStep: stepData.isAuthStep || false, authStepType: stepData.authStepType || null, created_at: new Date().toISOString() }; // Add to chat history this.chatHistory.push(message); // Persist updated chat history this.persistChatHistoryToStorage(); // Animate out the current module if it exists if (this.currentModule) { // Check if current module is a loading module const isLoadingModule = this.currentModule.dataset.moduleType === 'loading'; this.isAnimating = true; this.currentModule.classList.add('fade-out-up'); // Use shorter animation time for loading modules const animationTime = isLoadingModule ? 300 : 500; await this.wait(animationTime); this.container.innerHTML = ''; // Clear the container this.isAnimating = false; } // Display the message/step await this.displayMessage(message); // For responder types that auto-advance, pre-fetch the next step if (message.type === 'responder' && message.autoAdvance) { try { // Pre-fetch next step in background WITHOUT showing loading indicator const nextStepData = await this.requestNextStep(message.message_id, false); if (nextStepData) { // Store for auto-advance this._nextStepData = nextStepData; // Set timer for auto-advance this.autoAdvanceTimer = setTimeout(() => { this.handleResponderAutoAdvance(message, nextStepData); }, message.displayTime); } } catch (error) { console.error("Failed to pre-fetch next step:", error); // Continue showing current step even if pre-fetch fails } } } async handleResponderAutoAdvance(message, nextStepData) { // Clear the timer this.autoAdvanceTimer = null; // Create next message object const nextMessage = { message_id: nextStepData.message_id, title: nextStepData.title || '', message: nextStepData.message || nextStepData.title || '', type: nextStepData.type, options: nextStepData.options || [], input: null, required: nextStepData.required || false, autoAdvance: nextStepData.autoAdvance || false, displayTime: nextStepData.displayTime || 3000, responderText: nextStepData.responderText || null, explainer: nextStepData.explainer || null, created_at: new Date().toISOString() }; // Add to chat history this.chatHistory.push(nextMessage); // Persist updated chat history this.persistChatHistoryToStorage(); // First animate out current step await this.animateMessageOut(message.message_id); // Then display the next message await this.displayMessage(nextMessage); // ADDED: If the next message is also a responder with autoAdvance, set up its auto-advance if (nextMessage.type === 'responder' && nextMessage.autoAdvance) { try { // Pre-fetch next step in background WITHOUT showing loading indicator const subsequentStepData = await this.requestNextStep(nextMessage.message_id, false); if (subsequentStepData) { // Store for auto-advance this._nextStepData = subsequentStepData; // Set timer for auto-advance this.autoAdvanceTimer = setTimeout(() => { this.handleResponderAutoAdvance(nextMessage, subsequentStepData); }, nextMessage.displayTime); } } catch (error) { console.error("Failed to pre-fetch next step for subsequent responder:", error); // Continue showing current step even if pre-fetch fails } } } // Go back to previous step async handleBack() { if (this.isAnimating || this.chatHistory.length <= 1) return; this.isAnimating = true; // Set animating flag // Clean up any slide-up inputs this.cleanupSlideUpInputs(); // Get the previous message from history (excluding the current one) const currentMessage = this.chatHistory[this.chatHistory.length - 1]; const previousMessage = this.chatHistory[this.chatHistory.length - 2]; try { // First, animate out the current message const messageEl = this.container.querySelector(`[data-message-id="${currentMessage.message_id}"]`); if (messageEl) { messageEl.classList.add('fade-out-down'); await this.wait(500); // Wait for animation to complete } // Then remove the current message from history this.chatHistory.pop(); this.persistChatHistoryToStorage(); // Clear the container this.container.innerHTML = ''; // Display the previous message await this.displayMessage(previousMessage); // Update the navigation buttons state this.updateNextButtonState(); } catch (error) { console.error("Failed to go back:", error); this.showErrorMessage("Beim Zurückgehen ist ein Fehler aufgetreten."); } finally { this.isAnimating = false; // Clear animating flag } } async handleAuthStep(message, answer) { this.showLoadingIndicator('Verarbeite...'); console.log ("triggered handleAuthStep"); console.log (answer); try { switch (message.authStepType) { case 'email_collection': await this.handleEmailCollection(message, answer); break; case 'auth_method': await this.handleAuthMethod(message, answer); break; case 'verification_code': await this.handleVerificationCode(message, answer); break; case 'password_creation': await this.handlePasswordCreation(message, answer); break; default: // Unknown auth step type, continue normally const nextStepData = await this.requestNextStep(message.message_id); if (nextStepData) { this.processNextStep(nextStepData); } } } catch (error) { console.error("Auth step error:", error); this.showErrorMessage("Bei der Anmeldung ist ein Fehler aufgetreten."); } } async handleEmailCollection(message, email) { // Store email in auth state this.authState.email = email; // Check if email exists const formData = new FormData(); formData.append('email', email); try { const response = await fetch('https://api.hems-app.com/api:vjIzExhn/auth/check', { method: 'POST', mode: 'cors', credentials: 'include', body: formData }); if (!response.ok) { throw new Error(`Failed to check email: ${response.statusText}`); } const data = await response.json(); // Handle the three possible paths based on the next_step response switch (data.next_step) { case 'login': // User already has an account - redirect to login const loginMessage = document.createElement('div'); loginMessage.className = 'form-module redirect-module fade-in-up'; loginMessage.innerHTML = `
Es gibt schon ein Hems-Konto mit dieser E-Mail. Ich leite dich weiter zur Anmeldung...
`; // Add to container this.container.innerHTML = ''; this.container.appendChild(loginMessage); this.currentModule = loginMessage; // Redirect to login page // setTimeout(() => { // window.location.href = '/anmelden'; // }, 2500); break; case 'request_magic': // Account exists but needs verification const magicMessage = document.createElement('div'); magicMessage.className = 'form-module verification-module fade-in-up'; magicMessage.innerHTML = `
Dein Konto wurde bereits erstellt, aber noch nicht verifiziert. Wir senden dir einen Verifizierungscode.
`; this.container.innerHTML = ''; this.container.appendChild(magicMessage); this.currentModule = magicMessage; // Send verification email try { const verifyFormData = new FormData(); verifyFormData.append('email', email); verifyFormData.append('type', 'verification'); const verifyResponse = await fetch('https://api.hems-app.com/api:H8-XQm8i/auth/verify/resend', { method: 'POST', mode: 'cors', credentials: 'include', body: verifyFormData }); if (!verifyResponse.ok) { throw new Error('Failed to send verification email'); } // Proceed to verification code entry step const nextStepData = await this.requestNextStep(message.message_id); if (nextStepData) { this.processNextStep(nextStepData); } } catch (error) { console.error("Failed to send verification email:", error); this.showErrorMessage("Fehler beim Senden des Verifizierungscodes. Bitte versuche es später erneut.", true); } break; case 'register': default: // New user, proceed with regular signup flow const nextStepData = await this.requestNextStep(message.message_id); if (nextStepData) { this.processNextStep(nextStepData); } break; } } catch (error) { console.error("Email check failed:", error); this.showErrorMessage("Bei der Überprüfung deiner E-Mail ist ein Fehler aufgetreten.", true); } } async handleAuthMethod(message, method) { // Store auth method in state this.authState.authMethod = method; // Show loading indicator this.showLoadingIndicator('Verarbeite...'); try { // If passwordless was selected, create the account with passwordless=true if (method === 'passwordless') { try { this.showLoadingIndicator('Erstelle Konto...'); // Create account with passwordless=true (verification code will be sent automatically) await this.createAccount(); } catch (error) { console.error("Failed to create passwordless account:", error); this.showErrorMessage("Fehler bei der Kontoerstellung. Bitte versuche es erneut.", true); return; } } // For both passwordless and password methods, call next endpoint WITH the answer const nextResponse = await fetch(`${this.apiBaseUrl}${this.nextEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId, messageId: message.message_id, answer: method, // Pass the selected auth method as the answer chatHistory: this.chatHistory, contextToken: this.contextToken }) }); if (!nextResponse.ok) { throw new Error(`Failed to get next step: ${nextResponse.statusText}`); } const data = await nextResponse.json(); if (data.action === "next_step" && data.payload) { this.processNextStep(data.payload); } else if (data.action === "form_complete" && data.payload) { this.handleFormCompletion(data.payload); } else if (data.action === "error" && data.payload) { this.handleError(data.payload); } else { console.warn("Received unexpected response format:", data); this.showErrorMessage("Unerwartete Antwort vom Server erhalten."); } } catch (error) { console.error("Auth method handling error:", error); this.showErrorMessage("Bei der Verarbeitung ist ein Fehler aufgetreten.", true); } } async handleVerificationCode(message, code) { try { // Verify the code const formData = new FormData(); formData.append('email', this.authState.email); formData.append('code', code); const response = await fetch('https://api.hems-app.com/api:vjIzExhn/auth/verify', { method: 'POST', mode: 'cors', credentials: 'include', body: formData }); if (!response.ok) { const data = await response.json(); throw new Error(data.message || 'Verification failed'); } // Proceed to next step const nextStepData = await this.requestNextStep(message.message_id); if (nextStepData) { this.processNextStep(nextStepData); } } catch (error) { console.error("Verification failed:", error); this.showErrorMessage("Der eingegebene Code ist ungültig. Bitte versuche es erneut.", true); } } async handlePasswordCreation(message, password) { try { this.showLoadingIndicator('Erstelle Konto...'); // Create account with email and password await this.createAccount(password); // Proceed to next step const nextStepData = await this.requestNextStep(message.message_id); if (nextStepData) { this.processNextStep(nextStepData); } } catch (error) { console.error("Password creation failed:", error); this.showErrorMessage("Bei der Kontoerstellung ist ein Fehler aufgetreten.", true); } } async createAccount(password = null) { // Create FormData object const formData = new FormData(); // Add email formData.append('email', this.authState.email); // Add password if provided (for non-passwordless flow) if (password) { formData.append('password', password); } // Add passwordless flag formData.append('passwordless', this.authState.authMethod === 'passwordless' ? 'true' : 'false'); // Add product_preference formData.append('product_preference', 'homesearch'); // Add name if available (from a previous step) const nameMessage = this.chatHistory.find(msg => msg.authStepType === 'name_collection'); if (nameMessage && nameMessage.input) { formData.append('name', nameMessage.input); } console.log("Creating account with email:", this.authState.email, "passwordless:", this.authState.authMethod === 'passwordless'); const response = await fetch('https://api.hems-app.com/api:vjIzExhn/auth/register', { method: 'POST', body: formData, credentials: 'include', mode: 'cors' }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); console.error("Account creation failed:", response.status, errorData); throw new Error(`Failed to create account: ${response.statusText}`); } const data = await response.json(); // Update auth state this.authState.isAuthenticated = true; return data; } async resetAndStartNewSession() { // Clear local storage localStorage.removeItem('chatHistory'); localStorage.removeItem('sessionId'); localStorage.removeItem('contextToken'); localStorage.removeItem('sessionTimestamp'); // Create a new session const sessionData = await this.createSession(); this.sessionId = sessionData.session_id; localStorage.setItem('sessionId', this.sessionId); localStorage.setItem('sessionTimestamp', Date.now().toString()); this.chatHistory = []; // Start new session await this.startNewSession(); } async rebuildUIFromHistory() { try { // Show loading while we prepare // this.showLoadingIndicator('Lade Gespräch...'); // Make sure we have chat history if (this.chatHistory.length === 0) { await this.startNewSession(); return; } // Clear container before requesting next step this.container.innerHTML = ''; // Find the last message that has user input let lastMessageWithInput = null; for (let i = this.chatHistory.length - 1; i >= 0; i--) { if (this.chatHistory[i].input !== null) { lastMessageWithInput = this.chatHistory[i]; break; } } // If no message with input found, use the first message if (!lastMessageWithInput && this.chatHistory.length > 0) { lastMessageWithInput = this.chatHistory[0]; } if (lastMessageWithInput) { // Trim the history to remove any steps after the last input const lastInputIndex = this.chatHistory.findIndex(msg => msg.message_id === lastMessageWithInput.message_id); if (lastInputIndex !== -1) { // Keep only up to and including the last message with input this.chatHistory = this.chatHistory.slice(0, lastInputIndex + 1); } // Now request the next step properly const response = await fetch(`${this.apiBaseUrl}${this.nextEndpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: this.sessionId, messageId: lastMessageWithInput.message_id, answer: lastMessageWithInput.input, chatHistory: this.chatHistory, contextToken: this.contextToken }) }); if (!response.ok) { throw new Error(`Failed to continue session: ${response.statusText}`); } const data = await response.json(); if (data.action === "next_step" && data.payload) { this.processNextStep(data.payload); } else if (data.action === "form_complete" && data.payload) { this.handleFormCompletion(data.payload); } else if (data.action === "error" && data.payload) { this.handleError(data.payload); } else { await this.resetAndStartNewSession(); } } else { // No valid message found, start new await this.resetAndStartNewSession(); } // Persist the cleaned-up chat history this.persistChatHistoryToStorage(); } catch (error) { console.error("Error rebuilding UI from history:", error); await this.resetAndStartNewSession(); } } handleSessionExpired() { this.resetAndStartNewSession().catch(error => { console.error("Failed to create new session after expiration:", error); // If all else fails, reload the page window.location.reload(); }); } showSessionExpiredMessage() { // Instead of showing expired message UI, silently create a new session this.resetAndStartNewSession().catch(error => { console.error("Failed to create new session after expiration:", error); // If all else fails, reload the page window.location.reload(); }); } // Handle next button click async handleNext() { if (this.isAnimating) { // Cancel current animation this.cancelOngoingAnimations(); return; } window.scrollTo({ top: 0, behavior: 'smooth' }); if (this.nextButton.classList.contains('inactive')) { return; // Don't proceed if button is inactive } // Get current message if (this.chatHistory.length === 0) return; const currentMessage = this.chatHistory[this.chatHistory.length - 1]; // Skip validation for responder messages if (currentMessage.type !== 'responder' && currentMessage.required && !this.validateMessage(currentMessage)) { return; // Don't proceed if validation fails } // For responder types, just advance (already pre-fetched next step) if (currentMessage.type === 'responder') { // Cancel any auto-advance timer if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; } // If we have pre-fetched next step, use it if (this._nextStepData) { await this.handleResponderAutoAdvance(currentMessage, this._nextStepData); this._nextStepData = null; } else { // Otherwise, request next step (with loading indicator) const nextStepData = await this.requestNextStep(currentMessageId, true); if (nextStepData) { await this.handleResponderAutoAdvance(currentMessage, nextStepData); } } } else { // For input types, get user input and send const input = this.getUserInput(currentMessage); await this.sendUserInput(currentMessage.message_id, input); } } // Handle form completion handleFormCompletion(payload) { // Cancel any pending loading timers this.cancelLoadingTimer(); // Display completion message const completionMessage = document.createElement('div'); completionMessage.className = 'form-module'; completionMessage.innerHTML = `

Vielen Dank!

Deine Antworten werden übermittelt...
`; this.container.innerHTML = ''; this.container.appendChild(completionMessage); this.currentModule = completionMessage; // Create FormData object and add the three required fields const formData = new FormData(); formData.append('sessionToken', this.sessionId); formData.append('contextToken', this.contextToken); formData.append('chatHistory', JSON.stringify(this.chatHistory)); // Submit using FormData fetch('https://api.hems-app.com/api:fUxdXSnV/homesearch/submit', { method: 'POST', body: formData, credentials: 'include', mode: 'cors', }) .then(response => { if (!response.ok) { throw new Error('Submission failed'); } return response.json(); }) .then(data => { // Update success message completionMessage.innerHTML = `

Vielen Dank!

Deine Antworten wurden erfolgreich gespeichert.
`; // Clear localStorage localStorage.removeItem('chatHistory'); localStorage.removeItem('sessionId'); localStorage.removeItem('contextToken'); localStorage.removeItem('sessionTimestamp'); // Dispatch event const completionEvent = new CustomEvent('chat:complete', { detail: data }); document.dispatchEvent(completionEvent); // Handle redirection if API provided a redirect URL if (data.redirect_url) { setTimeout(() => { window.location.href = data.redirect_url; }, 1500); // 1.5 second delay to show success message } }) .catch(error => { console.error('Submission error:', error); completionMessage.innerHTML = `

Entschuldigung!

Bei der Übermittlung ist ein Fehler aufgetreten. Bitte versuche es erneut.
`; const retryButton = completionMessage.querySelector('.retry-button'); if (retryButton) { retryButton.addEventListener('click', () => { completionMessage.innerHTML = `

Vielen Dank!

Versuche erneut zu übermitteln...
`; setTimeout(() => this.handleFormCompletion(payload), 500); }); } }); } // Handle error handleError(payload) { // Cancel any pending loading timers this.cancelLoadingTimer(); console.error('Chat error:', payload.message); this.showErrorMessage(payload.message || 'Ein Fehler ist aufgetreten.'); } showLoadingIndicator(message = 'Lädt...') { // Clear any existing timer if (this.loadingTimer) { clearTimeout(this.loadingTimer); this.loadingTimer = null; } // For known slow operations, show immediately const isKnownSlowOperation = this.knownSlowOperations.includes(message); if (isKnownSlowOperation) { this._displayLoadingAnimation(message); } else { // Otherwise, set a timer to show loading only if operation takes longer than threshold this.loadingTimer = setTimeout(() => { this._displayLoadingAnimation(message); this.loadingTimer = null; }, this.loadingDelay); } } // Private method to actually display the loading animation _displayLoadingAnimation(message) { // First animate out current module if it exists if (this.currentModule) { this.currentModule.classList.add('fade-out-up'); // Wait briefly for animation to start before replacing setTimeout(() => { const loadingModule = document.createElement('div'); loadingModule.className = 'form-module loading-module fade-in-up'; loadingModule.dataset.moduleType = 'loading'; loadingModule.innerHTML = `
${message}
`; this.container.innerHTML = ''; this.container.appendChild(loadingModule); this.currentModule = loadingModule; }, 150); } else { // If no current module, just show the loading immediately const loadingModule = document.createElement('div'); loadingModule.className = 'form-module loading-module fade-in-up'; loadingModule.dataset.moduleType = 'loading'; loadingModule.innerHTML = `
${message}
`; this.container.innerHTML = ''; this.container.appendChild(loadingModule); this.currentModule = loadingModule; } } // Cancel loading timer cancelLoadingTimer() { if (this.loadingTimer) { clearTimeout(this.loadingTimer); this.loadingTimer = null; } } // Enable next button (active state) enableNextButton() { this.nextButton.classList.remove('inactive'); // Update arrow icon const arrowIcon = this.nextButton.querySelector('img'); if (arrowIcon) { arrowIcon.src = "https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/66ea8c6584c7e035cee00be0_right-arrow-wedge-next-white.svg"; } } updateNextButtonState() { // Skip if no chat history if (this.chatHistory.length === 0) return; const currentMessage = this.chatHistory[this.chatHistory.length - 1]; // Always enable for responder types if (currentMessage.type === 'responder') { this.enableNextButton(); return; } // If message is required, check if it's complete if (currentMessage.required) { if (this.isMessageComplete(currentMessage)) { this.enableNextButton(); } else { this.disableNextButton(); } } else { // Not required, always enable this.enableNextButton(); } } // Disable next button (inactive state) disableNextButton() { this.nextButton.classList.add('inactive'); // Update arrow icon const arrowIcon = this.nextButton.querySelector('img'); if (arrowIcon) { arrowIcon.src = "https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/6560739fd4d1fc9cf500057e_right-arrow-icon-mygesuch.svg"; } } showErrorMessage(message, isAuthError = false) { console.error("Error occurred:", message); if (isAuthError) { // For auth errors, show a specific error message const errorModule = document.createElement('div'); errorModule.className = 'form-module error-module fade-in-up'; errorModule.innerHTML = `
${message}
`; // Add to container this.container.innerHTML = ''; this.container.appendChild(errorModule); this.currentModule = errorModule; // Add retry button handler const retryButton = errorModule.querySelector('.error-retry-button'); if (retryButton) { retryButton.addEventListener('click', () => { // Go back to the previous step this.handleBack(); }); } } else { // For non-auth errors, use existing behavior (reset session) this.resetAndStartNewSession().catch(error => { console.error("Failed to create new session after error:", error); window.location.reload(); }); } } async displayMessage(message) { this.isAnimating = true; // Create module container const moduleEl = document.createElement('div'); moduleEl.className = 'form-module fade-in-up'; moduleEl.dataset.messageId = message.message_id; moduleEl.dataset.moduleType = 'message'; // Add title/heading if message has a title if (message.title) { const titleEl = document.createElement('h2'); titleEl.className = 'agent-create-ms-text title-text'; // Add special class if there's no message to add margin below if (!message.message || message.title === message.message) { titleEl.classList.add('title-no-message'); } moduleEl.appendChild(titleEl); // Will be animated later titleEl.textContent = ''; } // Add explanatory message if present if (message.message && message.title !== message.message) { // Avoid duplication if title and message are the same const messageEl = document.createElement('div'); messageEl.className = 'message-text'; moduleEl.appendChild(messageEl); // Always set to empty for consistent animation messageEl.textContent = ''; } // Add content container but initially hidden const contentEl = document.createElement('div'); contentEl.className = 'step-content'; contentEl.style.opacity = '0'; // Always start hidden moduleEl.appendChild(contentEl); // Generate specific content based on type switch (message.type) { case 'message-only': // No additional content break; case 'text-input': // Check if this is a specialized input type if (message.authStepType === 'verification_code') { contentEl.appendChild(this.createVerificationCodeInput(message)); } else if (message.authStepType === 'password_creation') { contentEl.appendChild(this.createPasswordInput(message)); } else { contentEl.appendChild(this.createTextInput(message)); } break; case 'date-input': contentEl.appendChild(this.createDateInput(message)); break; case 'money-input': contentEl.appendChild(this.createMoneyInput(message)); break; case 'select': contentEl.appendChild(this.createSelect(message)); // Add class for sequential animation of options const selectEl = contentEl.querySelector('select'); if (selectEl) { selectEl.classList.add('animate-select'); // Hide all options initially Array.from(selectEl.options).forEach((option, index) => { option.style.opacity = '0'; option.style.transition = 'opacity 0.3s ease-out'; option.dataset.animationIndex = index; }); } break; case 'checkbox-list': contentEl.appendChild(this.createCheckboxList(message)); // Add classes for sequential animation const checkboxes = contentEl.querySelectorAll('.selection-option'); checkboxes.forEach((option, index) => { option.classList.add('animate-option'); option.style.opacity = '0'; option.style.transform = 'translateY(10px)'; option.dataset.animationIndex = index; }); break; case 'radio-list': contentEl.appendChild(this.createRadioList(message)); // Add classes for sequential animation const radioOptions = contentEl.querySelectorAll('.selection-option'); radioOptions.forEach((option, index) => { option.classList.add('animate-option'); option.style.opacity = '0'; option.style.transform = 'translateY(10px)'; option.dataset.animationIndex = index; }); break; case 'responder': contentEl.appendChild(this.createResponder(message)); break; case 'loading': contentEl.appendChild(this.createLoadingAnimation()); break; default: console.warn(`Unknown message type: ${message.type}`); } if (message.explainer) { const explainerEl = document.createElement('p'); explainerEl.className = 'par-form gray explainer-fade'; explainerEl.style.marginTop = '48px'; // Set content immediately instead of empty explainerEl.textContent = message.explainer; // Start with hidden state for fade-in animation explainerEl.style.opacity = '0'; explainerEl.style.transform = 'translateY(15px)'; moduleEl.appendChild(explainerEl); } // Add to container this.container.appendChild(moduleEl); this.currentModule = moduleEl; // Ensure animation CSS classes take effect await this.wait(10); // Brief microtask delay // CONSISTENT ANIMATION SEQUENCE FOR ALL TYPES: this.updateNextButtonState(); // 1. Animate the title first and wait for it to complete if (message.title) { const titleEl = moduleEl.querySelector('.title-text'); if (titleEl) { // Ensure the title animation completes before showing other content await TextAnimator.animate(titleEl, message.title); } } // 2. Animate the message (if it exists and is different from title) if (message.message && message.title !== message.message) { const messageEl = moduleEl.querySelector('.message-text'); if (messageEl) { // Now animate the message TextAnimator.animate(messageEl, message.message); } } // 3. For responder types, handle responder text if (message.type === 'responder' && message.responderText) { const responderEl = contentEl.querySelector('.agent-create-ms-text'); if (responderEl) { contentEl.style.opacity = '1'; TextAnimator.animate(responderEl, message.responderText); } } // For all other types, fade in content else { // Show content container immediately contentEl.style.opacity = '1'; // Animate select options or list items sequentially if applicable if (['select', 'radio-list', 'checkbox-list'].includes(message.type)) { this.animateOptionsSequentially(contentEl, message.type); } } // 4. Animate the explainer text if present if (message.explainer) { const explainerEl = moduleEl.querySelector('.par-form.gray'); if (explainerEl) { // Animate the explainer text // Use CSS transition for fade in and up explainerEl.style.transition = 'opacity 0.8s ease-out, transform 0.8s ease-out'; explainerEl.style.opacity = '1'; explainerEl.style.transform = 'translateY(0)'; } } // Remove animation classes to prevent interference moduleEl.classList.remove('fade-in-up'); this.isAnimating = false; } // New method to animate options sequentially without blocking async animateOptionsSequentially(container, type) { if (type === 'select') { const select = container.querySelector('select.animate-select'); if (select) { const options = Array.from(select.options); // Skip the first option if it's a placeholder (usually is) const startIndex = options[0].disabled ? 1 : 0; // Use setTimeout instead of await to avoid blocking for (let i = startIndex; i < options.length; i++) { setTimeout(() => { options[i].style.opacity = '1'; }, (i - startIndex) * 70); // 70ms delay between each option } // Remove animation class after all animations complete setTimeout(() => { select.classList.remove('animate-select'); }, (options.length - startIndex) * 70 + 100); } } else if (type === 'radio-list' || type === 'checkbox-list') { const options = container.querySelectorAll('.selection-option.animate-option'); // Use setTimeout instead of await to avoid blocking options.forEach((option, i) => { setTimeout(() => { option.style.opacity = '1'; option.style.transform = 'translateY(0)'; option.classList.remove('animate-option'); }, i * 70); // 70ms delay between each option }); } } // Animate a message out async animateMessageOut(messageId) { const messageEl = this.container.querySelector(`[data-message-id="${messageId}"]`); if (!messageEl) return; this.isAnimating = true; // Animate out messageEl.classList.add('fade-out-up'); await this.wait(500); // Wait for animation to complete // Remove from DOM if (messageEl.parentNode) { messageEl.parentNode.removeChild(messageEl); } this.isAnimating = false; } // Clean up slide-up inputs cleanupSlideUpInputs() { const existingInputs = document.querySelectorAll('.slide-up-input-container'); existingInputs.forEach(input => { input.classList.add('slide-down-exit'); setTimeout(() => { if (input.parentNode) { input.parentNode.removeChild(input); } }, 300); }); // Remove body class document.body.classList.remove('has-slide-up-input'); // Remove spacing class from next button holder const nextButtonHolder = document.querySelector('.holder-next-button-modular-from'); if (nextButtonHolder) { nextButtonHolder.classList.remove('with-slide-input'); } this.currentSlideUpInput = null; } getUserInput(message) { if (!message) return null; const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`); if (!messageEl) return null; switch (message.type) { case 'text-input': // For verification code, use the hidden input if (message.authStepType === 'verification_code') { const hiddenInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`); return hiddenInput ? hiddenInput.value : null; } // For password input if (message.authStepType === 'password_creation') { const passwordInput = messageEl.querySelector(`input.cg-input-morph.pw[id="${message.message_id}"]`); return passwordInput ? passwordInput.value : null; } // For slide-up text input, check the body-level input const slideUpInput = document.querySelector(`#slide-input-${message.message_id} input[id="${message.message_id}"]`); if (slideUpInput) { return slideUpInput.value; } // For regular text input (fallback) const textInput = messageEl.querySelector(`input[type="text"][id="${message.message_id}"], input[type="password"][id="${message.message_id}"]`); return textInput ? textInput.value : null; case 'date-input': // For custom date input, return the hidden input value const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`); return hiddenDateInput ? hiddenDateInput.value : null; case 'money-input': const moneyInput = messageEl.querySelector(`#${message.message_id}`); if (!moneyInput) return null; // Return the numeric value, not the formatted string return this.parseMoneyValue(moneyInput.value); case 'select': const selectEl = messageEl.querySelector(`#${message.message_id}`); return selectEl ? selectEl.value : null; case 'radio-list': const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`); return selectedRadio ? selectedRadio.value : null; case 'checkbox-list': const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`); return selectedCheckboxes.length > 0 ? Array.from(selectedCheckboxes).map(cb => cb.value) : null; default: return null; } } // Cancel ongoing animations cancelOngoingAnimations() { // Clear auto-advance timer if it exists if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; } // Force completion of text animations if (this.currentModule) { // Find any elements being animated and reveal them instantly const textElements = this.currentModule.querySelectorAll('.agent-create-ms-text, .message-text, .explainer-text'); textElements.forEach(element => { // Get the message ID const messageId = this.currentModule.dataset.messageId; if (!messageId) return; // Find message in history const message = this.chatHistory.find(msg => msg.message_id === messageId); if (!message) return; if (element.closest('.step-content')) { // It's a responder text element.textContent = message.responderText || ''; } else if (element.classList.contains('explainer-text')) { // It's the explainer text element.textContent = message.explainer || ''; } else if (element.classList.contains('message-text')) { // It's the main message element.textContent = message.message || ''; } else if (element.classList.contains('title-text')) { // It's the title element.textContent = message.title || ''; } }); // Show content elements that might be waiting for animation const contentEl = this.currentModule.querySelector('.step-content'); if (contentEl) { contentEl.style.opacity = '1'; // Also reveal any options that might be animating const selectOptions = contentEl.querySelectorAll('select.animate-select option'); selectOptions.forEach(option => { option.style.opacity = '1'; }); const listOptions = contentEl.querySelectorAll('.selection-option.animate-option'); listOptions.forEach(option => { option.style.opacity = '1'; option.style.transform = 'translateY(0)'; }); } // Remove animation classes this.currentModule.classList.remove('fade-in-up', 'fade-in-down'); const selects = this.currentModule.querySelectorAll('select.animate-select'); selects.forEach(select => select.classList.remove('animate-select')); const options = this.currentModule.querySelectorAll('.selection-option.animate-option'); options.forEach(option => option.classList.remove('animate-option')); // Set isAnimating to false so we can proceed this.isAnimating = false; } } // Validate the current message validateMessage(message) { if (!message) return true; // No validation needed for certain types if (['message-only', 'responder'].includes(message.type)) return true; const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`); if (!messageEl) return true; let isValid = true; // Check for required fields based on type if (message.required) { switch (message.type) { case 'text-input': // Check slide-up input first const slideUpInput = document.querySelector(`#slide-input-${message.message_id} input[id="${message.message_id}"]`); const slideUpValidationMessage = document.querySelector(`#slide-input-${message.message_id} .slide-up-validation-error`); if (slideUpInput) { if (!slideUpInput.value.trim()) { if (slideUpValidationMessage) { slideUpValidationMessage.textContent = 'Dieses Feld ist erforderlich'; slideUpValidationMessage.style.display = 'block'; slideUpInput.style.borderColor = '#ff3b30'; } isValid = false; } else if (slideUpValidationMessage) { slideUpValidationMessage.style.display = 'none'; slideUpInput.style.borderColor = ''; } break; } // Fallback to regular text input const textInput = messageEl.querySelector(`#${message.message_id}`); const textValidationMessage = messageEl.querySelector('.validation-error'); if (!textInput || !textInput.value.trim()) { if (textValidationMessage) { textValidationMessage.textContent = 'Dieses Feld ist erforderlich'; textValidationMessage.style.display = 'block'; textValidationMessage.style.color = '#ff3b30'; textValidationMessage.style.fontSize = '0.875rem'; textValidationMessage.style.marginTop = '0.5rem'; // Highlight the input if (textInput) textInput.style.borderColor = '#ff3b30'; } isValid = false; } break; case 'date-input': // For custom date input, validate the hidden input value const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`); const dateInputGroup = messageEl.querySelector('.date-input-holder-group'); const validationMessage = messageEl.querySelector('.validation-error'); if (!hiddenDateInput || !hiddenDateInput.value) { if (validationMessage && dateInputGroup) { this.showCustomValidationMessage( validationMessage, dateInputGroup, 'Dieses Feld ist erforderlich' ); } isValid = false; } else if (validationMessage && dateInputGroup) { // Validate the date isValid = this.validateCustomDate( hiddenDateInput.value, message, validationMessage, dateInputGroup ); } break; case 'money-input': const moneyInput = messageEl.querySelector(`#${message.message_id}`); const moneyValidationMessage = messageEl.querySelector('.validation-error'); if (!moneyInput || !moneyInput.value.trim()) { if (moneyValidationMessage) { moneyValidationMessage.textContent = 'Bitte gib einen Betrag ein'; moneyValidationMessage.style.display = 'block'; moneyInput.style.borderColor = '#ff3b30'; } isValid = false; } else { // Check if it's a valid number const numericValue = this.parseMoneyValue(moneyInput.value); if (numericValue === null) { if (moneyValidationMessage) { moneyValidationMessage.textContent = 'Bitte gib einen gültigen Betrag ein'; moneyValidationMessage.style.display = 'block'; moneyInput.style.borderColor = '#ff3b30'; } isValid = false; } else if (message.minValue !== undefined && numericValue < message.minValue) { if (moneyValidationMessage) { moneyValidationMessage.textContent = `Der Mindestbetrag ist ${this.formatMoneyValue(message.minValue)}€`; moneyValidationMessage.style.display = 'block'; moneyInput.style.borderColor = '#ff3b30'; } isValid = false; } else if (message.maxValue !== undefined && numericValue > message.maxValue) { if (moneyValidationMessage) { moneyValidationMessage.textContent = `Der Höchstbetrag ist ${this.formatMoneyValue(message.maxValue)}€`; moneyValidationMessage.style.display = 'block'; moneyInput.style.borderColor = '#ff3b30'; } isValid = false; } } break; case 'select': const selectEl = messageEl.querySelector(`#${message.message_id}`); if (!selectEl || !selectEl.value) { this.showValidationError(selectEl, 'Bitte wähle eine Option'); isValid = false; } break; case 'radio-list': const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`); if (!selectedRadio) { const listEl = messageEl.querySelector('.selection-list'); if (listEl) { this.showValidationError(listEl, 'Bitte wähle eine Option'); } isValid = false; } break; case 'checkbox-list': const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`); if (selectedCheckboxes.length === 0) { const listEl = messageEl.querySelector('.selection-list'); if (listEl) { this.showValidationError(listEl, 'Bitte wähle mindestens eine Option'); } isValid = false; } break; } } return isValid; } isMessageComplete(message) { if (!message) return true; // No validation needed for certain types if (['message-only', 'responder'].includes(message.type)) return true; const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`); if (!messageEl) return true; switch (message.type) { case 'text-input': // Check slide-up input first const slideUpInput = document.querySelector(`#slide-input-${message.message_id} input[id="${message.message_id}"]`); if (slideUpInput) { return slideUpInput.value.trim() !== ''; } // Fallback to regular text input const textInputEl = messageEl.querySelector(`#${message.message_id}`); return textInputEl && textInputEl.value.trim() !== ''; case 'date-input': // For our custom date input, check the hidden input value const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`); return hiddenDateInput && hiddenDateInput.value !== ''; case 'money-input': const moneyInputEl = messageEl.querySelector(`#${message.message_id}`); const isComplete = moneyInputEl && moneyInputEl.value.trim() !== '' && this.parseMoneyValue(moneyInputEl.value) !== null; return isComplete; case 'select': const selectEl = messageEl.querySelector(`#${message.message_id}`); return selectEl && selectEl.value !== ''; case 'radio-list': const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`); return !!selectedRadio; case 'checkbox-list': const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`); return selectedCheckboxes.length > 0; default: return true; } } // Show validation error message showValidationError(element, message) { if (!element) return; // Remove existing error messages const existingError = element.parentNode.querySelector('.validation-error'); if (existingError) { existingError.parentNode.removeChild(existingError); } // Create and add error message const errorEl = document.createElement('div'); errorEl.className = 'validation-error'; errorEl.style.color = '#ff3b30'; errorEl.style.fontSize = '0.875rem'; errorEl.style.marginTop = '0.5rem'; errorEl.textContent = message; element.parentNode.appendChild(errorEl); // Highlight the input element.style.borderColor = '#ff3b30'; element.addEventListener('input', function onInput() { element.style.borderColor = ''; if (errorEl.parentNode) { errorEl.parentNode.removeChild(errorEl); } element.removeEventListener('input', onInput); }); } // Helper methods to create UI components // Create text input - Updated to trigger slide-up input createTextInput(message) { // For text inputs, we'll create a slide-up input from the bottom // Return an empty container since the input will be positioned separately const container = document.createElement('div'); container.className = 'text-input-placeholder'; // Create the slide-up input container setTimeout(() => { this.createSlideUpTextInput(message); }, 500); // Delay to show after content animation return container; } // Create slide-up text input from bottom createSlideUpTextInput(message) { // Create the slide-up input container const slideUpContainer = document.createElement('div'); slideUpContainer.className = 'slide-up-input-container'; slideUpContainer.id = `slide-input-${message.message_id}`; // Create the input wrapper const inputWrapper = document.createElement('div'); inputWrapper.className = 'slide-up-input-wrapper'; // Create the input with the specified classes const input = document.createElement('input'); input.type = 'text'; input.className = 'slide-up-text-input'; input.id = message.message_id; input.name = message.message_id; input.placeholder = message.placeholder || 'Type here...'; input.maxLength = 256; // Set value from stored input if available if (message.input !== null) { input.value = message.input; } input.addEventListener('focus', () => { setTimeout(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); document.documentElement.scrollTop = 0; // fallback }, 300); }); input.addEventListener('input', () => { this.updateNextButtonState(); setTimeout(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); document.documentElement.scrollTop = 0; }, 100); }); // Add validation message container (initially hidden) const validationMessage = document.createElement('div'); validationMessage.className = 'slide-up-validation-error'; validationMessage.style.display = 'none'; // Append elements to wrapper inputWrapper.appendChild(input); inputWrapper.appendChild(validationMessage); slideUpContainer.appendChild(inputWrapper); // Add body class for spacing document.body.classList.add('has-slide-up-input'); // Add spacing class to next button holder to prevent it being covered const nextButtonHolder = document.querySelector('.holder-next-button-modular-from'); if (nextButtonHolder) { nextButtonHolder.classList.add('with-slide-input'); } // Add to body to position at bottom document.body.appendChild(slideUpContainer); // Trigger slide-up animation setTimeout(() => { slideUpContainer.classList.add('slide-up-active'); // Focus the input after animation setTimeout(() => { input.focus(); }, 300); }, 100); // Store reference for cleanup this.currentSlideUpInput = slideUpContainer; } // Create verification code input with the specified structure createVerificationCodeInput(message) { // Outer container const spacerFrame = document.createElement('div'); spacerFrame.className = 'spacer-input-start-frame-cg'; // Code line spreader container const codeLineSpreader = document.createElement('div'); codeLineSpreader.className = 'cont-code-line-spreader'; spacerFrame.appendChild(codeLineSpreader); // Create 5 digit fields const digitCount = 5; const inputs = []; for (let i = 0; i < digitCount; i++) { // Container for each digit const lineHolder = document.createElement('div'); lineHolder.className = 'cont-hold-line-code'; // Create input for the digit const input = document.createElement('input'); input.type = 'text'; input.className = 'cg-input-morph-code w-input'; input.id = `verify-input-${i}`; // Make IDs unique input.name = 'verifycode-2'; input.dataset.name = 'Verifycode 2'; input.placeholder = 'X'; input.maxLength = 1; // Only allow one character input.autocomplete = 'off'; input.inputMode = 'numeric'; // Shows number keyboard on mobile // Only set autofocus on the first input if (i === 0) { input.autofocus = true; } // Create line element const lineElement = document.createElement('div'); lineElement.className = 'line-input-bottom-new'; // Add event listeners for auto-advancing input.addEventListener('input', (e) => { // Only accept digits 0-9 if (!/^\d?$/.test(e.target.value)) { e.target.value = ''; return; } // Move to next input if a digit was entered if (e.target.value !== '' && i < digitCount - 1) { inputs[i + 1].focus(); } // Update hidden field with complete code this.updateVerificationCode(message.message_id, inputs); // Update next button state this.updateNextButtonState(); }); // Handle backspace to go back to previous field input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && e.target.value === '' && i > 0) { inputs[i - 1].focus(); } }); inputs.push(input); // Append to line holder lineHolder.appendChild(input); lineHolder.appendChild(lineElement); // Add to code line spreader codeLineSpreader.appendChild(lineHolder); } // Add paste event listener to the code line spreader container codeLineSpreader.addEventListener('paste', (e) => { // Prevent the default paste behavior e.preventDefault(); // Get the pasted text from the clipboard const pastedText = (e.clipboardData || window.clipboardData).getData('text'); // Filter out non-digit characters const digits = pastedText.replace(/\D/g, ''); // Fill each input field with a digit from the pasted text for (let i = 0; i < inputs.length && i < digits.length; i++) { inputs[i].value = digits[i]; } // Update hidden field with complete code this.updateVerificationCode(message.message_id, inputs); // Focus the next empty field or the last field if all are filled let focusIndex = Math.min(digits.length, inputs.length - 1); inputs[focusIndex].focus(); // Update next button state this.updateNextButtonState(); }); // Add change event listeners to each input for browser autofill inputs.forEach((input, index) => { input.addEventListener('change', () => { // If the input has a value with more than one character (e.g., from autofill), // distribute the characters across the input fields if (input.value.length > 1) { const digits = input.value.replace(/\D/g, ''); for (let i = 0; i < inputs.length && i < digits.length; i++) { inputs[i].value = digits[i]; } // Update hidden value this.updateVerificationCode(message.message_id, inputs); // Focus the next empty field or the last field let focusIndex = Math.min(digits.length, inputs.length - 1); inputs[focusIndex].focus(); // Update next button state this.updateNextButtonState(); } }); }); // Add margin at the bottom const marginBottom = document.createElement('div'); marginBottom.className = 'margin-bottom margin-medium'; spacerFrame.appendChild(marginBottom); // Add hidden input to store the complete code const hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.id = message.message_id; hiddenInput.name = message.message_id; spacerFrame.appendChild(hiddenInput); // Set value from stored input if available if (message.input !== null) { hiddenInput.value = message.input; // Fill in the individual digit inputs const digits = message.input.split(''); digits.forEach((digit, index) => { if (index < inputs.length) { inputs[index].value = digit; } }); } // Add validation message container const validationMessage = document.createElement('div'); validationMessage.className = 'validation-error'; validationMessage.style.display = 'none'; validationMessage.style.textAlign = 'center'; validationMessage.style.marginTop = '10px'; spacerFrame.appendChild(validationMessage); return spacerFrame; } // Helper method to update the hidden field with the complete verification code updateVerificationCode(messageId, inputs) { const code = inputs.map(input => input.value).join(''); const hiddenInput = document.querySelector(`input[type="hidden"][id="${messageId}"]`); if (hiddenInput) { hiddenInput.value = code; } } // Create date input createDateInput(message) { // Create date input structure with 3 fields const dateInputGroup = document.createElement('div'); dateInputGroup.className = 'date-input-holder-group'; // Create day input const dayInputHolder = document.createElement('div'); dayInputHolder.className = 'date-input-holder'; const dayMorphCont = document.createElement('div'); dayMorphCont.className = 'date-morph-cont day'; const dayInput = document.createElement('input'); dayInput.type = 'text'; dayInput.className = 'cg-input-morph w-input date-input-transition'; dayInput.id = 'day'; dayInput.name = 'day'; dayInput.placeholder = 'Tag'; dayInput.maxLength = 256; dayInput.setAttribute('data-name', 'day'); dayInput.autofocus = true; const dayLine = document.createElement('div'); dayLine.className = 'line-input-bottom-new'; const dayError = document.createElement('div'); dayError.className = 'date-input-error'; dayError.id = 'dayError'; dayError.style.display = 'none'; dayError.textContent = 'Bitte trage eine korrekte Zahl ein (1-31)'; // Create month input const monthInputHolder = document.createElement('div'); monthInputHolder.className = 'date-input-holder'; const monthMorphCont = document.createElement('div'); monthMorphCont.className = 'date-morph-cont month'; const monthInput = document.createElement('input'); monthInput.type = 'text'; monthInput.className = 'cg-input-morph w-input date-input-transition'; monthInput.id = 'month'; monthInput.name = 'month'; monthInput.placeholder = 'Monat'; monthInput.maxLength = 256; monthInput.setAttribute('data-name', 'month'); const monthLine = document.createElement('div'); monthLine.className = 'line-input-bottom-new'; const monthError = document.createElement('div'); monthError.className = 'date-input-error'; monthError.id = 'monthError'; monthError.style.display = 'none'; monthError.textContent = 'Bitte trage einen gültigen Monat ein'; // Create year input const yearInputHolder = document.createElement('div'); yearInputHolder.className = 'date-input-holder'; const yearMorphCont = document.createElement('div'); yearMorphCont.className = 'date-morph-cont year'; const yearInput = document.createElement('input'); yearInput.type = 'text'; yearInput.className = 'cg-input-morph w-input date-input-transition'; yearInput.id = 'year'; yearInput.name = 'year'; yearInput.placeholder = 'Jahr'; yearInput.maxLength = 256; yearInput.setAttribute('data-name', 'year'); const yearLine = document.createElement('div'); yearLine.className = 'line-input-bottom-new'; const yearError = document.createElement('div'); yearError.className = 'date-input-error'; yearError.id = 'yearError'; yearError.style.display = 'none'; yearError.textContent = 'Bitte trage ein gültiges Jahr ein'; // Add inputs to their holders dayMorphCont.appendChild(dayInput); dayMorphCont.appendChild(dayLine); dayMorphCont.appendChild(dayError); dayInputHolder.appendChild(dayMorphCont); monthMorphCont.appendChild(monthInput); monthMorphCont.appendChild(monthLine); monthMorphCont.appendChild(monthError); monthInputHolder.appendChild(monthMorphCont); yearMorphCont.appendChild(yearInput); yearMorphCont.appendChild(yearLine); yearMorphCont.appendChild(yearError); yearInputHolder.appendChild(yearMorphCont); // Add all holders to the group dateInputGroup.appendChild(dayInputHolder); dateInputGroup.appendChild(monthInputHolder); dateInputGroup.appendChild(yearInputHolder); // Add hidden input to store the full date value (YYYY-MM-DD format) const hiddenDateInput = document.createElement('input'); hiddenDateInput.type = 'hidden'; hiddenDateInput.id = message.message_id; hiddenDateInput.name = message.message_id; // Set value from stored input if available if (message.input !== null) { hiddenDateInput.value = message.input; this.populateDateFields(message.input); // Note the "this." prefix } const container = document.createElement('div'); container.className = 'date-input-container'; container.appendChild(dateInputGroup); container.appendChild(hiddenDateInput); // Set up event listeners for all date fields this.setupDateFieldEvents(message, container, dateInputGroup); // Note the "this." prefix return container; } // Create password input with toggle visibility createPasswordInput(message) { // Main container const holderMorphPw = document.createElement('div'); holderMorphPw.className = 'holder-morph-pw-input'; // Password input container const contMorphPassword = document.createElement('div'); contMorphPassword.className = 'cont-morph-password'; holderMorphPw.appendChild(contMorphPassword); // Create password input const input = document.createElement('input'); input.type = 'password'; // Start as password type input.className = 'cg-input-morph pw w-input'; input.id = message.message_id; input.name = 'field-2'; input.dataset.name = 'Field 2'; input.placeholder = '*******'; input.autofocus = true; input.maxLength = 256; input.required = message.required || false; // Set value from stored input if available if (message.input !== null) { input.value = message.input; } // Add event listener for input changes input.addEventListener('input', () => this.updateNextButtonState()); // Create toggle visibility button const toggleButton = document.createElement('div'); toggleButton.id = 'toggleaddPasswordVisibility'; toggleButton.className = 'cta-hide-show-password-lp morph'; // Create eye icon const eyeIcon = document.createElement('img'); eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg'; eyeIcon.loading = 'lazy'; eyeIcon.id = 'eyeicon'; eyeIcon.alt = ''; eyeIcon.className = 'icon-hideshow-password-lp'; // Add click event to toggle password visibility toggleButton.addEventListener('click', () => { if (input.type === 'password') { input.type = 'text'; eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg'; // Use crossed-eye icon for visible password } else { input.type = 'password'; eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg'; // Use regular eye icon for hidden password } }); // Append elements toggleButton.appendChild(eyeIcon); contMorphPassword.appendChild(input); contMorphPassword.appendChild(toggleButton); // Add bottom line const lineElement = document.createElement('div'); lineElement.className = 'line-input-bottom-new'; holderMorphPw.appendChild(lineElement); // Add validation message container (initially hidden) const validationMessage = document.createElement('div'); validationMessage.className = 'validation-error'; validationMessage.style.display = 'none'; holderMorphPw.appendChild(validationMessage); return holderMorphPw; } // Helper method to create individual date fields createDateField(id, placeholder, autofocus = false) { const input = document.createElement('input'); input.className = 'date-field'; input.type = 'text'; input.maxLength = '1'; input.placeholder = placeholder; input.id = id; input.name = id; input.dataset.name = id; if (autofocus) { input.autofocus = true; } // Allow only numbers input.addEventListener('keypress', (e) => { const keyCode = e.which || e.keyCode; if (keyCode < 48 || keyCode > 57) { e.preventDefault(); } }); return input; } // Create money input createMoneyInput(message) { const container = document.createElement('div'); container.className = 'cont-morph-input money-input-container'; // Create input wrapper with currency symbol const inputWrapper = document.createElement('div'); inputWrapper.className = 'money-input-wrapper'; // Add euro symbol before input const currencySymbol = document.createElement('span'); currencySymbol.className = 'currency-symbol'; currencySymbol.textContent = '€'; // Create the input const input = document.createElement('input'); input.type = 'text'; input.className = 'cg-input-morph money-input w-input'; input.id = message.message_id; input.name = message.message_id; input.placeholder = message.placeholder || 'Betrag eingeben'; input.maxLength = 15; // Allow enough chars for large amounts input.autocomplete = 'off'; input.inputMode = 'numeric'; // Shows number keyboard on mobile // Set value from stored input if available if (message.input !== null) { input.value = this.formatMoneyValue(message.input); } // Add event listeners for input changes and formatting input.addEventListener('input', (e) => { // Only allow digits, commas, dots, and spaces let value = e.target.value.replace(/[^0-9.,\s]/g, ''); // Replace dots with commas for European format value = value.replace(/\./g, ','); // Only allow one comma const commaIndex = value.indexOf(','); if (commaIndex !== -1) { const beforeComma = value.substring(0, commaIndex); let afterComma = value.substring(commaIndex + 1); // Limit to 2 decimal places if (afterComma.length > 2) { afterComma = afterComma.substring(0, 2); } value = beforeComma + ',' + afterComma; } // Update the input value e.target.value = value; // Update next button state this.updateNextButtonState(); }); // Format the value on blur input.addEventListener('blur', (e) => { const numericValue = this.parseMoneyValue(e.target.value); if (numericValue !== null) { e.target.value = this.formatMoneyValue(numericValue); } }); // Create the line element const lineElement = document.createElement('div'); lineElement.className = 'line-input-bottom-new'; // Add validation message container const validationMessage = document.createElement('div'); validationMessage.className = 'validation-error'; validationMessage.style.display = 'none'; // Assemble the components inputWrapper.appendChild(input); container.appendChild(inputWrapper); container.appendChild(lineElement); container.appendChild(validationMessage); return container; } // Helper to format money value for display formatMoneyValue(value) { if (value === null || value === undefined || value === '') { return ''; } // If value is a string with a comma, parse it first if (typeof value === 'string' && value.includes(',')) { value = this.parseMoneyValue(value); if (value === null) return ''; } // Format the number with German/European formatting (1.234,56) return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value); } // Helper to parse money value from formatted string parseMoneyValue(value) { if (!value) return null; // Remove any currency symbols, spaces value = value.replace(/[€\s]/g, ''); // Convert European format to a format parseFloat can handle value = value.replace(/\./g, '').replace(',', '.'); const numValue = parseFloat(value); return isNaN(numValue) ? null : numValue; } // Add validation for money input validateMoneyInput(message) { const messageEl = document.querySelector(`[data-message-id="${message.message_id}"]`); if (!messageEl) return true; const input = messageEl.querySelector(`#${message.message_id}`); const validationMessage = messageEl.querySelector('.validation-error'); if (!input || !validationMessage) return true; // Clear previous validation validationMessage.style.display = 'none'; input.style.borderColor = ''; // Skip validation if empty and not required if (!input.value.trim() && !message.required) { return true; } // Required check if (message.required && !input.value.trim()) { validationMessage.textContent = 'Bitte gib einen Betrag ein'; validationMessage.style.display = 'block'; input.style.borderColor = '#ff3b30'; return false; } // Parse the value const numericValue = this.parseMoneyValue(input.value); // Check if valid number if (numericValue === null) { validationMessage.textContent = 'Bitte gib einen gültigen Betrag ein'; validationMessage.style.display = 'block'; input.style.borderColor = '#ff3b30'; return false; } // Check minimum value if specified if (message.minValue !== undefined && numericValue < message.minValue) { validationMessage.textContent = `Der Mindestbetrag ist ${this.formatMoneyValue(message.minValue)} €`; validationMessage.style.display = 'block'; input.style.borderColor = '#ff3b30'; return false; } // Check maximum value if specified if (message.maxValue !== undefined && numericValue > message.maxValue) { validationMessage.textContent = `Der Höchstbetrag ist ${this.formatMoneyValue(message.maxValue)} €`; validationMessage.style.display = 'block'; input.style.borderColor = '#ff3b30'; return false; } return true; } animateShake(element) { if (!element) return; element.classList.remove('date-input-shake'); void element.offsetWidth; // Trigger reflow element.classList.add('date-input-shake'); setTimeout(() => { if (element) element.classList.remove('date-input-shake'); }, 600); } animatePulse(element) { if (!element) return; element.classList.remove('date-input-pulse'); void element.offsetWidth; // Trigger reflow element.classList.add('date-input-pulse'); setTimeout(() => { if (element) element.classList.remove('date-input-pulse'); }, 1600); } animateHighlight(element) { if (!element) return; element.classList.remove('date-input-highlight'); void element.offsetWidth; // Trigger reflow element.classList.add('date-input-highlight'); setTimeout(() => { if (element) element.classList.remove('date-input-highlight'); }, 1100); } // Setup event listeners for date fields setupDateFieldEvents(message, container, dateInputGroup) { // Get the 3 input fields const dayInput = container.querySelector('#day'); const monthInput = container.querySelector('#month'); const yearInput = container.querySelector('#year'); const hiddenInput = container.querySelector(`input[id="${message.message_id}"]`); const validationMessage = document.createElement('div'); validationMessage.className = 'validation-error'; validationMessage.style.display = 'none'; container.appendChild(validationMessage); // Current date for reference const currentDate = new Date(); const currentYear = currentDate.getFullYear(); const currentMonth = currentDate.getMonth() + 1; // JavaScript months are 0-based // Track if backward navigation is in progress let backNavigationInProgress = false; // Track month input for text entry let lastMonthInputValue = ''; // Basic validation limits const yearMin = 1900; const yearMax = currentMonth >= 10 ? currentYear + 1 : currentYear; // COMMON KEYBOARD NAVIGATION // Day input keyboard navigation dayInput.addEventListener('keydown', (e) => { // Right arrow to month if (e.key === 'ArrowRight' && dayInput.selectionStart === dayInput.value.length) { e.preventDefault(); monthInput.focus(); monthInput.setSelectionRange(0, 0); } }); // Month input keyboard navigation monthInput.addEventListener('keydown', (e) => { // Left arrow to day if (e.key === 'ArrowLeft' && monthInput.selectionStart === 0) { e.preventDefault(); backNavigationInProgress = true; dayInput.focus(); dayInput.setSelectionRange(dayInput.value.length, dayInput.value.length); setTimeout(() => { backNavigationInProgress = false; }, 100); } // Right arrow to year else if (e.key === 'ArrowRight' && monthInput.selectionStart === monthInput.value.length) { e.preventDefault(); yearInput.focus(); yearInput.setSelectionRange(0, 0); } // Backspace in empty field - go back to day else if (e.key === 'Backspace' && monthInput.value === '') { e.preventDefault(); backNavigationInProgress = true; dayInput.focus(); dayInput.setSelectionRange(dayInput.value.length, dayInput.value.length); setTimeout(() => { backNavigationInProgress = false; }, 100); } }); // Year input keyboard navigation yearInput.addEventListener('keydown', (e) => { // Left arrow to month if (e.key === 'ArrowLeft' && yearInput.selectionStart === 0) { e.preventDefault(); backNavigationInProgress = true; monthInput.focus(); monthInput.setSelectionRange(monthInput.value.length, monthInput.value.length); setTimeout(() => { backNavigationInProgress = false; }, 100); } // Backspace in empty field - go back to month else if (e.key === 'Backspace' && yearInput.value === '') { e.preventDefault(); backNavigationInProgress = true; monthInput.focus(); monthInput.setSelectionRange(monthInput.value.length, monthInput.value.length); setTimeout(() => { backNavigationInProgress = false; }, 100); } }); // DAY INPUT HANDLING dayInput.addEventListener('input', (e) => { let value = e.target.value.replace(/\D/g, ''); // Remove non-digits // Limit to 2 digits if (value.length > 2) { value = value.slice(0, 2); } // Cap at 31 if (parseInt(value, 10) > 31) { value = '31'; this.animateShake(dayInput); } e.target.value = value; // Clear validation message validationMessage.style.display = 'none'; dateInputGroup.style.borderColor = ''; // Update hidden field this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); // Update next button state this.updateNextButtonState(); // Auto-advance to month if valid 2-digit day if (value.length === 2 && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 31) { setTimeout(() => { monthInput.focus(); this.animatePulse(monthInput); }, 10); } }); // Validate and format day on blur dayInput.addEventListener('blur', (e) => { const value = e.target.value.trim(); if (value === '') { return; // Don't validate empty field } // Validate day const day = parseInt(value, 10); let month = 0; // Try to get month value if available if (monthInput.value) { if (/^\d+$/.test(monthInput.value)) { month = parseInt(monthInput.value, 10); } else { const monthNum = findClosestMonth(monthInput.value); if (monthNum) month = monthNum; } } const year = parseInt(yearInput.value, 10) || 0; const isValid = !isNaN(day) && isValidDay(day, month, year); if (!isValid) { validationMessage.textContent = 'Ungültiger Tag'; validationMessage.style.display = 'block'; this.animateShake(dayInput); } else { // Pad single digit with zero if (value.length === 1) { e.target.value = value.padStart(2, '0'); this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); } } }); // MONTH INPUT HANDLING monthInput.addEventListener('input', (e) => { const value = e.target.value.trim(); // Track if user is deleting const isDeleting = value.length < lastMonthInputValue.length; lastMonthInputValue = value; // Clear validation validationMessage.style.display = 'none'; dateInputGroup.style.borderColor = ''; // Numeric input handling if (/^\d+$/.test(value)) { let numValue = value; // Cap at 12 if (parseInt(numValue, 10) > 12) { numValue = '12'; e.target.value = numValue; this.animateShake(monthInput); } // Auto-advance after valid 2-digit month if (numValue.length === 2 && parseInt(numValue, 10) >= 1 && parseInt(numValue, 10) <= 12) { setTimeout(() => { yearInput.focus(); this.animatePulse(yearInput); }, 10); } // Update hidden value this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); this.updateNextButtonState(); return; } // Text-based month handling if (!isDeleting) { // Try to match to a month name const monthNum = findClosestMonth(value); // If we have a full valid match, move to year if (value.length >= 3 && monthNum !== null) { // Check for exact match const matchesExactly = Object.keys(germanMonths).some(month => month === value.toLowerCase() && germanMonths[month] === monthNum ); if (matchesExactly) { setTimeout(() => { yearInput.focus(); this.animatePulse(yearInput); }, 10); } } } // Update hidden value this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); this.updateNextButtonState(); }); // Format and validate month on blur monthInput.addEventListener('blur', (e) => { lastMonthInputValue = ''; // Reset tracker const value = e.target.value.trim(); if (value === '') { return; // Don't validate empty } // For numeric input if (/^\d+$/.test(value)) { const monthNum = parseInt(value, 10); const isValid = !isNaN(monthNum) && isValidMonth(monthNum); if (!isValid) { validationMessage.textContent = 'Ungültiger Monat'; validationMessage.style.display = 'block'; this.animateShake(monthInput); } else { // Format single-digit months if (value.length === 1) { e.target.value = value.padStart(2, '0'); } this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); } return; } // For text input const monthNum = findClosestMonth(value); const isValid = monthNum !== null && isValidMonth(monthNum); if (!isValid) { validationMessage.textContent = 'Ungültiger Monat'; validationMessage.style.display = 'block'; this.animateShake(monthInput); } else { // Format as full month name const formattedMonth = fullMonthNames.de[monthNum - 1]; e.target.value = formattedMonth; this.animateHighlight(monthInput); this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); } }); // YEAR INPUT HANDLING yearInput.addEventListener('input', (e) => { let value = e.target.value.replace(/\D/g, ''); // Remove non-digits // Limit to 4 digits if (value.length > 4) { value = value.slice(0, 4); } e.target.value = value; // Clear validation validationMessage.style.display = 'none'; dateInputGroup.style.borderColor = ''; // Update hidden value this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); this.updateNextButtonState(); // Auto-validate when 4 digits are entered if (value.length === 4) { const year = parseInt(value, 10); if (year >= yearMin && year <= yearMax) { setTimeout(() => { yearInput.blur(); this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup); }, 300); } } }); // Auto-fill year on blur yearInput.addEventListener('blur', (e) => { // Don't auto-fill if navigating back if (backNavigationInProgress) { return; } const value = e.target.value.trim(); if (value === '') { // Auto-fill current year if day & month are filled if (dayInput.value && monthInput.value) { // Get month value to determine if we should use next year let monthValue = monthInput.value; let monthNum; if (/^\d+$/.test(monthValue)) { monthNum = parseInt(monthValue, 10); } else { monthNum = findClosestMonth(monthValue); } // For late-year months (Nov/Dec), consider next year if ((monthNum === 11 || monthNum === 12) && currentMonth >= 10) { e.target.value = (currentYear + 1).toString(); } else { e.target.value = currentYear.toString(); } this.animateHighlight(yearInput); this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); } return; } // Validate year const year = parseInt(value, 10); if (!isNaN(year)) { if (year < yearMin) { e.target.value = yearMin.toString(); this.animateShake(yearInput); } else if (year > yearMax) { e.target.value = yearMax.toString(); this.animateShake(yearInput); } else { // Ensure 4 digits e.target.value = year.toString().padStart(4, '0'); } this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput); // Validate if this completes the date if (dayInput.value && monthInput.value) { this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup); } } }); // Debounced validation function const validateDateDelayed = this.debounce(() => { if (dayInput.value && monthInput.value && yearInput.value) { this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup); } }, 800); // Attach validation to all fields dayInput.addEventListener('input', validateDateDelayed); monthInput.addEventListener('input', validateDateDelayed); yearInput.addEventListener('input', validateDateDelayed); } // Populate date fields from a date string (YYYY-MM-DD) populateDateFields(dateString) { if (!dateString || dateString.length !== 10) return; try { // Parse YYYY-MM-DD format const year = dateString.substring(0, 4); const month = dateString.substring(5, 7); const day = dateString.substring(8, 10); const dayInput = document.querySelector('#day'); const monthInput = document.querySelector('#month'); const yearInput = document.querySelector('#year'); if (dayInput) { dayInput.value = parseInt(day, 10).toString().padStart(2, '0'); } if (monthInput) { monthInput.value = parseInt(month, 10).toString().padStart(2, '0'); } if (yearInput) { yearInput.value = year; } } catch (e) { console.error('Error populating date fields:', e); } } validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup) { const day = parseInt(dayInput.value, 10); let month = monthInput.value.trim(); const year = parseInt(yearInput.value, 10); // Try to parse month as number or name let monthNum; if (/^\d+$/.test(month)) { monthNum = parseInt(month, 10); } else { monthNum = findClosestMonth(month); } // Check if all fields are valid const dayValid = !isNaN(day) && isValidDay(day, monthNum, year); const monthValid = monthNum !== null && isValidMonth(monthNum); const yearValid = !isNaN(year) && year >= 1900 && year <= (new Date().getFullYear() + 1); // If any part is invalid, show validation message if (!dayValid) { validationMessage.textContent = 'Ungültiger Tag für diesen Monat'; validationMessage.style.display = 'block'; this.animateShake(dayInput); return false; } if (!monthValid) { validationMessage.textContent = 'Ungültiger Monat'; validationMessage.style.display = 'block'; this.animateShake(monthInput); return false; } if (!yearValid) { validationMessage.textContent = 'Ungültiges Jahr'; validationMessage.style.display = 'block'; this.animateShake(yearInput); return false; } // If all valid, clear validation and return true validationMessage.style.display = 'none'; dateInputGroup.style.borderColor = ''; return true; } // Check if all date fields are filled isDateInputComplete(dateFields) { for (let i = 0; i < dateFields.length; i++) { if (!dateFields[i].value) { return false; } } return true; } // Update hidden input with the complete date value updateHiddenDateValue(messageId, dayInput, monthInput, yearInput, hiddenInput) { // Get values const dayValue = dayInput.value.trim(); const monthValue = monthInput.value.trim(); const yearValue = yearInput.value.trim(); // Only update if we have at least partial values in all fields if (dayValue && monthValue && yearValue) { // Parse month value - could be text or number let monthNum; if (/^\d+$/.test(monthValue)) { monthNum = parseInt(monthValue, 10); } else { monthNum = findClosestMonth(monthValue); } // Only set if we have valid numbers const day = parseInt(dayValue, 10); const year = parseInt(yearValue, 10); if (!isNaN(day) && monthNum !== null && !isNaN(year)) { // Format as YYYY-MM-DD const formattedMonth = monthNum.toString().padStart(2, '0'); const formattedDay = day.toString().padStart(2, '0'); hiddenInput.value = `${year}-${formattedMonth}-${formattedDay}`; } } else { // Clear if incomplete hiddenInput.value = ''; } } // Validate the custom date input validateCustomDate(dateValue, message, validationEl, dateInputGroup) { // Check if input is empty when required if (message.required && !dateValue) { this.showCustomValidationMessage(validationEl, dateInputGroup, 'Dieses Feld ist erforderlich'); this.animateShake(dateInputGroup); return false; } // If input has a value, validate date format and constraints if (dateValue) { const selectedDate = new Date(dateValue); // Check if date is valid if (isNaN(selectedDate.getTime())) { this.showCustomValidationMessage(validationEl, dateInputGroup, 'Ungültiges Datum'); this.animateShake(dateInputGroup); return false; } // Extract date parts for advanced validation const [year, month, day] = dateValue.split('-').map(Number); // Validate day for this specific month/year if (!isValidDay(day, month, year)) { this.showCustomValidationMessage(validationEl, dateInputGroup, 'Ungültiger Tag für diesen Monat'); this.animateShake(dateInputGroup); return false; } // Check min date constraint if (message.minDate && new Date(dateValue) < new Date(message.minDate)) { this.showCustomValidationMessage( validationEl, dateInputGroup, `Datum muss nach ${this.formatDate(message.minDate)} sein` ); this.animateShake(dateInputGroup); return false; } // Check max date constraint if (message.maxDate && new Date(dateValue) > new Date(message.maxDate)) { this.showCustomValidationMessage( validationEl, dateInputGroup, `Datum muss vor ${this.formatDate(message.maxDate)} sein` ); this.animateShake(dateInputGroup); return false; } // Custom validation logic from message if provided if (message.validate && typeof message.validate === 'function') { const customValidation = message.validate(dateValue); if (customValidation !== true) { this.showCustomValidationMessage( validationEl, dateInputGroup, customValidation || 'Ungültiges Datum' ); this.animateShake(dateInputGroup); return false; } } } return true; } isDateInputComplete(dateFields) { for (let i = 0; i < dateFields.length; i++) { if (!dateFields[i].value) { return false; } } return true; } showCustomValidationMessage(validationEl, dateInputGroup, message) { // Check if validationEl exists before using it if (!validationEl) { console.warn('Validation element is null. Cannot display validation message.'); return; } // Check if dateInputGroup exists before using it if (!dateInputGroup) { console.warn('Date input group is null. Cannot highlight date fields.'); } else { dateInputGroup.style.borderColor = '#ff3b30'; } validationEl.textContent = message; validationEl.style.display = 'block'; validationEl.style.color = '#ff3b30'; validationEl.style.fontSize = '0.875rem'; validationEl.style.marginTop = '0.5rem'; } // Add a specific validation method for date inputs validateDateInput(input, message, validationEl) { // Check if input is empty when required if (message.required && (!input.value || input.value.trim() === '')) { this.showValidationMessage(validationEl, input, 'Dieses Feld ist erforderlich'); return false; } // If input has a value, validate date format and constraints if (input.value) { const selectedDate = new Date(input.value); // Check if date is valid if (isNaN(selectedDate.getTime())) { this.showValidationMessage(validationEl, input, 'Ungültiges Datum'); return false; } // Check min date constraint if (input.min && new Date(input.value) < new Date(input.min)) { this.showValidationMessage(validationEl, input, `Datum muss nach ${this.formatDate(input.min)} sein`); return false; } // Check max date constraint if (input.max && new Date(input.value) > new Date(input.max)) { this.showValidationMessage(validationEl, input, `Datum muss vor ${this.formatDate(input.max)} sein`); return false; } // Custom validation logic from message if provided if (message.validate && typeof message.validate === 'function') { const customValidation = message.validate(input.value); if (customValidation !== true) { this.showValidationMessage(validationEl, input, customValidation || 'Ungültiges Datum'); return false; } } } return true; } // Helper method to show validation messages showValidationMessage(validationEl, input, message) { validationEl.textContent = message; validationEl.style.display = 'block'; validationEl.style.color = '#ff3b30'; validationEl.style.fontSize = '0.875rem'; validationEl.style.marginTop = '0.5rem'; // Highlight the input input.style.borderColor = '#ff3b30'; } // Helper method to format dates for error messages formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } // Create select dropdown createSelect(message) { const container = document.createElement('div'); container.className = 'input-field'; const select = document.createElement('select'); select.id = message.message_id; select.name = message.message_id; // Add placeholder option const placeholderOption = document.createElement('option'); placeholderOption.value = ''; placeholderOption.textContent = message.placeholder || 'Bitte wählen'; placeholderOption.disabled = true; placeholderOption.selected = message.input === null; select.appendChild(placeholderOption); // Add options if (message.options && Array.isArray(message.options)) { message.options.forEach(option => { const optionEl = document.createElement('option'); optionEl.value = option.value; optionEl.textContent = option.label; // Set selected if it matches saved input if (message.input === option.value) { optionEl.selected = true; } select.appendChild(optionEl); }); } input.addEventListener('change', () => this.updateNextButtonState()); container.appendChild(select); return container; } // Create checkbox list createCheckboxList(message) { const container = document.createElement('div'); container.className = 'selection-list'; // Add options if (message.options && Array.isArray(message.options)) { message.options.forEach(option => { const optionContainer = document.createElement('div'); optionContainer.className = 'selection-option'; const labelEl = document.createElement('span'); labelEl.className = 'selection-option-label'; labelEl.textContent = option.label; const input = document.createElement('input'); input.type = 'checkbox'; input.id = `${message.message_id}_${option.value}`; input.name = message.message_id; input.value = option.value; // Add custom checkbox visual const checkboxEl = document.createElement('span'); checkboxEl.className = 'selection-checkbox'; // Check if this option is already selected if (message.input && Array.isArray(message.input) && message.input.includes(option.value)) { input.checked = true; } // Check if this option should be disabled/dimmed if (option.disabled) { optionContainer.classList.add('selection-option-dimmed'); input.disabled = true; } input.addEventListener('change', () => this.updateNextButtonState()); optionContainer.appendChild(labelEl); optionContainer.appendChild(input); optionContainer.appendChild(checkboxEl); container.appendChild(optionContainer); // Make entire option clickable optionContainer.addEventListener('click', (e) => { if (!option.disabled && e.target !== input) { input.checked = !input.checked; // Trigger a change event so any listeners can respond input.dispatchEvent(new Event('change')); } }); }); } return container; } // Create radio list createRadioList(message) { const container = document.createElement('div'); container.className = 'selection-list'; // Add options if (message.options && Array.isArray(message.options)) { message.options.forEach(option => { const optionContainer = document.createElement('div'); optionContainer.className = 'selection-option'; const labelEl = document.createElement('span'); labelEl.className = 'selection-option-label'; labelEl.textContent = option.label; const input = document.createElement('input'); input.type = 'radio'; input.id = `${message.message_id}_${option.value}`; input.name = message.message_id; input.value = option.value; // Add custom radio visual const radioEl = document.createElement('span'); radioEl.className = 'selection-radio'; // Check if this option is already selected if (message.input === option.value) { input.checked = true; } // Check if this option should be disabled/dimmed if (option.disabled) { optionContainer.classList.add('selection-option-dimmed'); input.disabled = true; } optionContainer.appendChild(labelEl); optionContainer.appendChild(input); optionContainer.appendChild(radioEl); container.appendChild(optionContainer); // Make entire option clickable optionContainer.addEventListener('click', (e) => { if (!option.disabled && e.target !== input) { input.checked = true; // Trigger a change event so any listeners can respond input.dispatchEvent(new Event('change')); } }); input.addEventListener('change', () => this.updateNextButtonState()); // Add auto-advance functionality if the message has autoAdvanceOnSelect enabled if (message.autoAdvanceOnSelect) { input.addEventListener('change', (e) => { if (e.target.checked) { // Short delay to allow the user to see their selection before advancing setTimeout(() => { this.handleNext(); }, 300); } }); } }); } return container; } // Create responder createResponder(message) { const container = document.createElement('div'); container.className = 'agent-create-ms-text'; container.textContent = ''; return container; } // Create loading animation createLoadingAnimation(message = 'Lade...') { const container = document.createElement('div'); container.className = 'loading-animation'; // Create spinner const spinner = document.createElement('div'); spinner.className = 'spinner'; container.appendChild(spinner); // Create loading text const loadingText = document.createElement('div'); loadingText.textContent = message; container.appendChild(loadingText); return container; } // Enhancement to cancelOngoingAnimations to clean up animation classes cancelOngoingAnimations() { // Clear auto-advance timer if it exists if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; } // Force completion of text animations if (this.currentModule) { // Find any elements being animated and reveal them instantly const textElements = this.currentModule.querySelectorAll('.agent-create-ms-text, .message-text, .par-form.gray'); textElements.forEach(element => { // Get the message ID const messageId = this.currentModule.dataset.messageId; if (!messageId) return; // Find message in history const message = this.chatHistory.find(msg => msg.message_id === messageId); if (!message) return; if (element.closest('.step-content')) { // It's a responder text element.textContent = message.responderText || ''; } else if (element.classList.contains('par-form')) { // It's the explainer text - simply ensure it's fully visible element.style.opacity = '1'; element.style.transform = 'translateY(0)'; // Content is already set when created } else if (element.classList.contains('message-text')) { // It's the main message element.textContent = message.message || ''; } else if (element.classList.contains('title-text')) { // It's the title element.textContent = message.title || ''; } }); // Show content elements that might be waiting for animation const contentEl = this.currentModule.querySelector('.step-content'); if (contentEl) { contentEl.style.opacity = '1'; // Also reveal any options that might be animating const selectOptions = contentEl.querySelectorAll('select.animate-select option'); selectOptions.forEach(option => { option.style.opacity = '1'; }); const listOptions = contentEl.querySelectorAll('.selection-option.animate-option'); listOptions.forEach(option => { option.style.opacity = '1'; option.style.transform = 'translateY(0)'; }); } // Remove animation classes this.currentModule.classList.remove('fade-in-up', 'fade-in-down'); const selects = this.currentModule.querySelectorAll('select.animate-select'); selects.forEach(select => select.classList.remove('animate-select')); const options = this.currentModule.querySelectorAll('.selection-option.animate-option'); options.forEach(option => option.classList.remove('animate-option')); // Set isAnimating to false so we can proceed this.isAnimating = false; } } // Inject CSS styles injectStyles() { const styleEl = document.createElement('style'); styleEl.textContent = ` .form-module { width: 100%; opacity: 1; transform: translateY(0); transition: all 0.5s ease-in-out; max-width: 600px; margin: 0 auto; margin-bottom: 20px; } .fade-out-up { opacity: 0; transform: translateY(-30px); } .fade-out-down { opacity: 0; transform: translateY(30px); } .fade-in-up { animation: fadeInUp 0.5s ease-out forwards; } .fade-in-down { animation: fadeInDown 0.5s ease-out forwards; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeInDown { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } } .message-text { font-size: 0.9rem; line-height: 1.5; margin-top: 0.7rem; margin-bottom: 1rem; color: #333; } .step-content { opacity: 0; transition: opacity 0.5s ease-out; margin-top: 0.7rem; margin-bottom: 1rem; } .selection-list { width: 100%; } .selection-option { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-bottom: 1px solid #eee; cursor: pointer; transition: all 0.3s ease; } .selection-option.animate-option { transition: opacity 0.3s ease-out, transform 0.3s ease-out; } select.animate-select option { transition: opacity 0.3s ease-out; } .selection-option:last-child { border-bottom: none; } .selection-option-label { font-size: 1rem; color: #000; } .selection-option-dimmed { color: #999; } .selection-radio { width: 24px; height: 24px; border-radius: 50%; border: 2px solid #ddd; background-color: transparent; position: relative; transition: all 0.2s ease; flex-shrink: 0; } .selection-checkbox { width: 24px; height: 24px; border-radius: 5px; border: 2px solid #ddd; background-color: transparent; position: relative; transition: all 0.2s ease; flex-shrink: 0; } .selection-option input { position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; } .selection-option input:checked ~ .selection-radio { border-color: #C99A6D; background-color: #C99A6D; } .selection-option input:checked ~ .selection-checkbox { border-color: #C99A6D; background-color: #C99A6D; } .selection-option input:checked ~ .selection-option-label { font-weight: 500; } /* Checkmark for checkbox */ .selection-option input:checked ~ .selection-checkbox:after { content: ""; position: absolute; display: block; left: 8px; top: 4px; width: 5px; height: 10px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } /* Center dot for radio */ .selection-option input:checked ~ .selection-radio:after { content: ""; position: absolute; display: block; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 10px; height: 10px; border-radius: 50%; background: white; } .input-field { margin-bottom: 1.5rem; transition: opacity 0.3s ease-out, transform 0.3s ease-out; } .input-field label { display: block; margin-bottom: 0.5rem; font-size: 0.9rem; color: #666; } .input-field input, .input-field select { width: 100%; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; transition: border-color 0.2s ease; } .input-field input:focus, .input-field select:focus { outline: none; border-color: #C99A6D; } .date-input { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; width: 100%; font-size: 1rem; } .date-input:focus { outline: none; border-color: #C99A6D; } .loading-animation { display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 2rem 0; } .spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #C99A6D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Additional styles for form UI */ .resume-container { padding: 20px; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; } .resume-progress { margin: 20px 0; } .progress-bar { height: 10px; background-color: #e0e0e0; border-radius: 5px; overflow: hidden; } .progress-fill { height: 100%; background-color: #C99A6D; border-radius: 5px; } .progress-text { margin-top: 5px; font-size: 14px; color: #666; } .resume-buttons { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } .resume-button { padding: 10px 20px; border-radius: 4px; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s ease; } .resume-yes { background-color: #C99A6D; color: white; } .resume-no { background-color: #f0f0f0; color: #333; } .error-container, .expired-container { padding: 20px; text-align: center; background-color: #fff0f0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .expired-container { background-color: #f9f9f9; } .error-icon, .expired-icon { font-size: 36px; margin-bottom: 10px; } .error-message { margin-bottom: 20px; color: #d32f2f; } .error-retry-button, .restart-button { padding: 10px 20px; background-color: #C99A6D; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; } .loading-module { min-height: 150px; display: flex; align-items: center; justify-content: center; } .form-module.fade-in-up { animation: fadeInUp 0.5s ease-out forwards; opacity: 0; transform: translateY(30px); } .form-module.fade-in-down { animation: fadeInDown 0.5s ease-out forwards; opacity: 0; transform: translateY(-30px); } .loading-animation { display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 2rem 0; transition: all 0.3s ease-out; } .spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #C99A6D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 1rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Animation specifically for loading transitions */ .loading-module.fade-out-up { animation: fadeOutUp 0.3s ease-out forwards; } .form-module.fade-in-up { animation: fadeInUp 0.5s ease-out forwards; opacity: 0; transform: translateY(30px); } .form-module.fade-out-up { animation: fadeOutUp 0.5s ease-out forwards; } @keyframes fadeOutUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-30px); } } .explainer-fade { opacity: 0; transform: translateY(15px); transition: opacity 0.8s ease-out, transform 0.8s ease-out; } @keyframes shakeAnimation { 0% { transform: translateX(0); } 20% { transform: translateX(-5px); } 40% { transform: translateX(5px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } 100% { transform: translateX(0); } } @keyframes pulseAnimation { 0% { box-shadow: 0 0 0 0 rgba(201, 154, 109, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(201, 154, 109, 0); } 100% { box-shadow: 0 0 0 0 rgba(201, 154, 109, 0); } } @keyframes highlightAnimation { 0% { background-color: rgba(255, 213, 79, 0); } 50% { background-color: rgba(255, 213, 79, 0.3); } 100% { background-color: rgba(255, 213, 79, 0); } } .date-input-shake { animation: shakeAnimation 0.5s cubic-bezier(.36,.07,.19,.97) both; border-color: #ff3b30 !important; } .date-input-pulse { animation: pulseAnimation 1.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; border-color: #C99A6D !important; } .date-input-highlight { animation: highlightAnimation 1s ease-in-out both; } .date-input-holder-group { transition: all 0.3s ease; } .date-field { transition: all 0.3s ease; } /* Slide-up input styles */ .slide-up-input-container { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-top: 1px solid #eee; border-radius: 15px 15px 0px 0px; padding: 20px; padding-bottom: calc(50px + env(safe-area-inset-bottom)); transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); z-index: 1000; box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1); } .slide-up-input-container.slide-up-active { transform: translateY(0); } .slide-up-input-container.slide-down-exit { transform: translateY(100%); } .slide-up-input-wrapper { max-width: 600px; margin: 0 auto; position: relative; } .slide-up-text-input { width: 100%; border: none; font-size: 1.2rem; background:white; outline: none; transition: all 0.2s ease; box-sizing: border-box; } .slide-up-text-input:focus { border-color: #C99A6D; background:white; } .slide-up-text-input::placeholder { color: #999; font-size: 1.2rem; } .slide-up-validation-error { color: #ff3b30; font-size: 0.875rem; margin-top: 0.5rem; text-align: center; } .text-input-placeholder { min-height: 20px; } /* Ensure content doesn't get hidden behind slide-up input */ body.has-slide-up-input { padding-bottom: 120px; } /* Next button spacing when slide-up input is active - targets specific class */ .holder-next-button-modular-from.with-slide-input { margin-bottom: 4rem !important; /* Space for slide-up input + extra padding */ transition: margin-bottom 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); } `; document.head.appendChild(styleEl); } // Helper method to wait for a specified time wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Initialize the chat-based form function initializeHemsChat() { const formController = new ChatBasedFormController(); // Initialize the form formController.initialize(); // Make form controller globally accessible for debugging window.formController = formController; return formController; } // Start the form when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { initializeHemsChat(); });