// 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 = `
`;
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 = `
`;
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();
});