Spaces:
Running
Running
| // Verify MC Patient Intake Widget | |
| // Production-grade, conversion-optimized form with AI assistance | |
| (function() { | |
| 'use strict'; | |
| // State management | |
| const state = { | |
| formData: {}, | |
| aiProcessing: false, | |
| sections: { | |
| personal: { fields: 5, filled: 0 }, | |
| emergency: { fields: 3, filled: 0 }, | |
| insurance: { fields: 4, filled: 0 }, | |
| medical: { fields: 4, filled: 0 }, | |
| visit: { fields: 2, filled: 0 } | |
| }, | |
| totalFields: 18, | |
| filledFields: 0 | |
| }; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', init); | |
| function init() { | |
| loadSavedData(); | |
| attachEventListeners(); | |
| updateProgress(); | |
| initializePhoneFormatting(); | |
| } | |
| // Event Listeners | |
| function attachEventListeners() { | |
| const form = document.getElementById('intakeForm'); | |
| const inputs = form.querySelectorAll('input, select, textarea'); | |
| inputs.forEach(input => { | |
| // Real-time validation and progress | |
| input.addEventListener('blur', () => validateField(input)); | |
| input.addEventListener('input', () => { | |
| updateFieldStatus(input); | |
| debouncedProgressUpdate(); | |
| }); | |
| // AI suggestion on focus for certain fields | |
| if (input.name === 'symptoms' || input.name === 'medications') { | |
| input.addEventListener('focus', () => showAIHint(input)); | |
| } | |
| }); | |
| // Enter key in AI input | |
| document.getElementById('aiInput').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') processAIInput(); | |
| }); | |
| // Form submission prevention | |
| form.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| submitForm(); | |
| }); | |
| } | |
| // Phone number formatting | |
| function initializePhoneFormatting() { | |
| const phoneInputs = document.querySelectorAll('input[type="tel"]'); | |
| phoneInputs.forEach(input => { | |
| input.addEventListener('input', (e) => { | |
| let value = e.target.value.replace(/\D/g, ''); | |
| if (value.length >= 6) { | |
| value = `(${value.slice(0, 3)}) ${value.slice(3, 6)}-${value.slice(6, 10)}`; | |
| } else if (value.length >= 3) { | |
| value = `(${value.slice(0, 3)}) ${value.slice(3)}`; | |
| } | |
| e.target.value = value; | |
| }); | |
| }); | |
| } | |
| // Field validation | |
| function validateField(input) { | |
| const isRequired = input.hasAttribute('required'); | |
| const isEmpty = !input.value.trim(); | |
| if (isRequired && isEmpty) { | |
| input.classList.add('error'); | |
| showFieldError(input, 'This field is required'); | |
| } else if (input.type === 'email' && input.value && !isValidEmail(input.value)) { | |
| input.classList.add('error'); | |
| showFieldError(input, 'Please enter a valid email'); | |
| } else { | |
| input.classList.remove('error'); | |
| clearFieldError(input); | |
| } | |
| } | |
| function isValidEmail(email) { | |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); | |
| } | |
| function showFieldError(input, message) { | |
| clearFieldError(input); | |
| const error = document.createElement('p'); | |
| error.className = 'field-error text-verify-error text-xs mt-1.5 flex items-center gap-1'; | |
| error.innerHTML = `<i data-lucide="alert-circle" class="w-3 h-3"></i>${message}`; | |
| input.parentNode.appendChild(error); | |
| lucide.createIcons(); | |
| } | |
| function clearFieldError(input) { | |
| const existing = input.parentNode.querySelector('.field-error'); | |
| if (existing) existing.remove(); | |
| } | |
| // Progress tracking | |
| function updateFieldStatus(input) { | |
| const section = input.dataset.section; | |
| if (!section) return; | |
| const isFilled = input.type === 'checkbox' | |
| ? input.checked | |
| : input.value.trim().length > 0; | |
| state.formData[input.name] = isFilled ? input.value : null; | |
| // Auto-save to localStorage | |
| debouncedSave(); | |
| } | |
| let progressDebounceTimer; | |
| function debouncedProgressUpdate() { | |
| clearTimeout(progressDebounceTimer); | |
| progressDebounceTimer = setTimeout(updateProgress, 100); | |
| } | |
| function updateProgress() { | |
| const sections = ['personal', 'emergency', 'insurance', 'medical', 'visit']; | |
| let totalFilled = 0; | |
| let totalCount = 0; | |
| sections.forEach(sectionKey => { | |
| const sectionEl = document.querySelector(`[data-section="${sectionKey}"]`)?.closest('section'); | |
| if (!sectionEl) return; | |
| const fields = sectionEl.querySelectorAll('input, select, textarea'); | |
| let filled = 0; | |
| let count = 0; | |
| fields.forEach(field => { | |
| // Skip checkboxes for simple counting, focus on text inputs | |
| if (field.type === 'checkbox') return; | |
| count++; | |
| totalCount++; | |
| const isFilled = field.value.trim().length > 0; | |
| if (isFilled) { | |
| filled++; | |
| totalFilled++; | |
| } | |
| }); | |
| // Update section progress indicator | |
| const progressEl = document.getElementById(`${sectionKey}Progress`); | |
| if (progressEl) { | |
| progressEl.textContent = `${filled}/${count}`; | |
| if (filled === count) { | |
| progressEl.className = 'text-verify-teal text-xs ml-auto font-medium'; | |
| } else { | |
| progressEl.className = 'text-verify-muted text-xs ml-auto'; | |
| } | |
| } | |
| }); | |
| // Update main progress bars | |
| const percentage = Math.round((totalFilled / totalCount) * 100) || 0; | |
| document.getElementById('progressBar').style.width = `${percentage}%`; | |
| document.getElementById('bottomProgress').style.width = `${percentage}%`; | |
| document.getElementById('completionText').textContent = `${percentage}% complete`; | |
| // Visual feedback on completion milestones | |
| if (percentage === 100 && !state.completedShown) { | |
| state.completedShown = true; | |
| showToast('All fields completed! Ready to submit.'); | |
| } | |
| } | |
| // AI Processing | |
| window.processAIInput = async function() { | |
| const input = document.getElementById('aiInput'); | |
| const btn = document.getElementById('aiTryBtn'); | |
| const suggestions = document.getElementById('aiSuggestions'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| // Show processing state | |
| state.aiProcessing = true; | |
| btn.disabled = true; | |
| btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Processing...</span>`; | |
| lucide.createIcons(); | |
| // Simulate AI processing (in production, this would call an API) | |
| await new Promise(r => setTimeout(r, 1500)); | |
| // Parse and extract information | |
| const extracted = extractInfoFromText(text); | |
| // Display suggestions | |
| displayAISuggestions(extracted, suggestions); | |
| // Reset button | |
| btn.disabled = false; | |
| btn.innerHTML = `<i data-lucide="wand-2" class="w-4 h-4"></i><span class="hidden sm:inline">Try it</span>`; | |
| lucide.createIcons(); | |
| state.aiProcessing = false; | |
| }; | |
| function extractInfoFromText(text) { | |
| const info = {}; | |
| const lower = text.toLowerCase(); | |
| // Name extraction | |
| const nameMatch = text.match(/(?:name is|i am|my name is)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i) || | |
| text.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/); | |
| if (nameMatch) info.fullName = nameMatch[1]; | |
| // Date extraction (various formats) | |
| const dateMatch = text.match(/(?:born|birth|dob|birthday)[\s,]*([A-Za-z]+\s+\d{1,2}[,.]?\s*\d{4}|\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4})/i); | |
| if (dateMatch) { | |
| info.dateOfBirth = parseDate(dateMatch[1]); | |
| } | |
| // Phone extraction | |
| const phoneMatch = text.match(/(\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})/); | |
| if (phoneMatch) info.phone = phoneMatch[1]; | |
| // Email extraction | |
| const emailMatch = text.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/); | |
| if (emailMatch) info.email = emailMatch[1]; | |
| // Allergies | |
| if (lower.includes('allergic to') || lower.includes('allergy')) { | |
| const allergyMatch = text.match(/allergic to\s+([^,.]+)/i); | |
| if (allergyMatch) info.allergies = allergyMatch[1]; | |
| } | |
| // Medications | |
| if (lower.includes('take') || lower.includes('medication')) { | |
| const medMatch = text.match(/(?:take|taking|on)\s+([^,.]+(?:mg|daily|twice|once)[^,.]*)/i); | |
| if (medMatch) info.medications = medMatch[1]; | |
| } | |
| // Insurance | |
| const insuranceKeywords = { | |
| 'aetna': 'aetna', | |
| 'anthem': 'anthem', | |
| 'blue cross': 'bcbs', | |
| 'bluecross': 'bcbs', | |
| 'cigna': 'cigna', | |
| 'kaiser': 'kaiser', | |
| 'medicare': 'medicare', | |
| 'medicaid': 'medicaid', | |
| 'united': 'united' | |
| }; | |
| for (const [keyword, value] of Object.entries(insuranceKeywords)) { | |
| if (lower.includes(keyword)) { | |
| info.insuranceProvider = value; | |
| break; | |
| } | |
| } | |
| // Conditions | |
| const conditions = []; | |
| if (lower.includes('diabetes')) conditions.push('diabetes'); | |
| if (lower.includes('hypertension') || lower.includes('high blood pressure')) conditions.push('hypertension'); | |
| if (lower.includes('asthma')) conditions.push('asthma'); | |
| if (lower.includes('heart')) conditions.push('heart-disease'); | |
| if (conditions.length) info.conditions = conditions; | |
| return info; | |
| } | |
| function parseDate(dateStr) { | |
| // Try to parse various date formats and return YYYY-MM-DD | |
| const tryParse = new Date(dateStr); | |
| if (!isNaN(tryParse)) { | |
| return tryParse.toISOString().split('T')[0]; | |
| } | |
| return null; | |
| } | |
| function displayAISuggestions(extracted, container) { | |
| container.innerHTML = ''; | |
| container.classList.remove('hidden'); | |
| const suggestions = Object.entries(extracted).filter(([key, value]) => value); | |
| if (suggestions.length === 0) { | |
| container.innerHTML = ` | |
| <div class="ai-suggestion p-3 bg-amber-50 border border-amber-200 rounded-xl flex items-center gap-2"> | |
| <i data-lucide="alert-circle" class="w-4 h-4 text-amber-600"></i> | |
| <span class="text-amber-800 text-sm">We couldn't extract specific information. Try being more specific with names, dates, and numbers.</span> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| return; | |
| } | |
| suggestions.forEach(([field, value]) => { | |
| const div = document.createElement('div'); | |
| div.className = 'ai-suggestion p-3 bg-white border border-verify-teal/30 rounded-xl flex items-center justify-between gap-3'; | |
| const displayField = field.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); | |
| const displayValue = Array.isArray(value) ? value.join(', ') : value; | |
| div.innerHTML = ` | |
| <div class="flex items-center gap-2 flex-1"> | |
| <i data-lucide="check-circle-2" class="w-4 h-4 text-verify-teal flex-shrink-0"></i> | |
| <div class="min-w-0"> | |
| <span class="text-verify-muted text-xs">${displayField}</span> | |
| <p class="text-verify-text text-sm font-medium truncate">${displayValue}</p> | |
| </div> | |
| </div> | |
| <button onclick="applyAIValue('${field}', '${Array.isArray(value) ? value.join(',') : value}')" | |
| class="px-3 py-1.5 bg-verify-teal text-white text-xs font-medium rounded-lg hover:bg-verify-teal-hover transition-colors flex-shrink-0"> | |
| Apply | |
| </button> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| window.applyAIValue = function(field, value) { | |
| const input = document.querySelector(`[name="${field}"]`); | |
| if (!input) return; | |
| if (field === 'conditions') { | |
| const conditions = value.split(','); | |
| conditions.forEach(c => { | |
| const checkbox = document.querySelector(`[name="conditions"][value="${c}"]`); | |
| if (checkbox) checkbox.checked = true; | |
| }); | |
| } else { | |
| input.value = value; | |
| } | |
| updateFieldStatus(input); | |
| updateProgress(); | |
| // Remove applied suggestion | |
| const suggestions = document.getElementById('aiSuggestions'); | |
| const applied = suggestions.querySelector(`button[onclick*="${field}"]`)?.closest('.ai-suggestion'); | |
| if (applied) { | |
| applied.style.opacity = '0.5'; | |
| applied.querySelector('button').textContent = 'Applied'; | |
| applied.querySelector('button').disabled = true; | |
| } | |
| showToast(`${field.replace(/([A-Z])/g, ' $1')} applied`); | |
| }; | |
| // Allergy tag toggle | |
| window.toggleAllergy = function(btn, allergy) { | |
| const input = document.getElementById('allergiesInput'); | |
| const current = input.value.split(',').map(s => s.trim()).filter(s => s); | |
| if (btn.classList.contains('bg-verify-teal')) { | |
| // Remove | |
| btn.classList.remove('bg-verify-teal', 'text-white'); | |
| btn.classList.add('bg-gray-100', 'text-verify-muted'); | |
| const idx = current.indexOf(allergy); | |
| if (idx > -1) current.splice(idx, 1); | |
| } else { | |
| // Add | |
| btn.classList.remove('bg-gray-100', 'text-verify-muted'); | |
| btn.classList.add('bg-verify-teal', 'text-white'); | |
| if (!current.includes(allergy)) current.push(allergy); | |
| } | |
| input.value = current.join(', '); | |
| updateFieldStatus(input); | |
| }; | |
| // Form submission | |
| window.submitForm = async function() { | |
| const form = document.getElementById('intakeForm'); | |
| const required = form.querySelectorAll('[required]'); | |
| let valid = true; | |
| required.forEach(field => { | |
| validateField(field); | |
| if (!field.value.trim()) valid = false; | |
| }); | |
| if (!valid) { | |
| showToast('Please fill in all required fields'); | |
| // Scroll to first error | |
| const firstError = form.querySelector('.error'); | |
| if (firstError) { | |
| firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| firstError.focus(); | |
| } | |
| return; | |
| } | |
| // Simulate submission | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| const originalText = submitBtn.innerHTML; | |
| submitBtn.disabled = true; | |
| submitBtn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Submitting...</span>`; | |
| lucide.createIcons(); | |
| await new Promise(r => setTimeout(r, 2000)); | |
| // Clear saved data | |
| localStorage.removeItem('verifyMC_intakeDraft'); | |
| // Show success | |
| document.getElementById('successModal').classList.remove('hidden'); | |
| document.getElementById('successModal').classList.add('flex'); | |
| }; | |
| window.closeModal = function() { | |
| document.getElementById('successModal').classList.add('hidden'); | |
| document.getElementById('successModal').classList.remove('flex'); | |
| // Reset form | |
| document.getElementById('intakeForm').reset(); | |
| updateProgress(); | |
| }; | |
| // Draft saving | |
| window.saveDraft = function() { | |
| const form = document.getElementById('intakeForm'); | |
| const data = new FormData(form); | |
| const saved = {}; | |
| for (const [key, value] of data.entries()) { | |
| if (saved[key]) { | |
| if (!Array.isArray(saved[key])) saved[key] = [saved[key]]; | |
| saved[key].push(value); | |
| } else { | |
| saved[key] = value; | |
| } | |
| } | |
| localStorage.setItem('verifyMC_intakeDraft', JSON.stringify(saved)); | |
| showToast('Draft saved — you can return anytime'); | |
| }; | |
| function loadSavedData() { | |
| const saved = localStorage.getItem('verifyMC_intakeDraft'); | |
| if (!saved) return; | |
| try { | |
| const data = JSON.parse(saved); | |
| Object.entries(data).forEach(([key, value]) => { | |
| const field = document.querySelector(`[name="${key}"]`); | |
| if (!field) return; | |
| if (field.type === 'checkbox') { | |
| field.checked = true; | |
| } else if (Array.isArray(value)) { | |
| // Handle multi-select checkboxes | |
| value.forEach(v => { | |
| const cb = document.querySelector(`[name="${key}"][value="${v}"]`); | |
| if (cb) cb.checked = true; | |
| }); | |
| } else { | |
| field.value = value; | |
| } | |
| }); | |
| } catch (e) { | |
| console.error('Failed to load saved data:', e); | |
| } | |
| } | |
| let saveDebounceTimer; | |
| function debouncedSave() { | |
| clearTimeout(saveDebounceTimer); | |
| saveDebounceTimer = setTimeout(() => { | |
| const form = document.getElementById('intakeForm'); | |
| const data = new FormData(form); | |
| const saved = {}; | |
| for (const [key, value] of data.entries()) { | |
| saved[key] = value; | |
| } | |
| localStorage.setItem('verifyMC_intakeDraft', JSON.stringify(saved)); | |
| }, 2000); | |
| } | |
| // Toast notifications | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| document.getElementById('toastMessage').textContent = message; | |
| toast.classList.remove('hidden'); | |
| toast.classList.add('flex'); | |
| setTimeout(() => { | |
| toast.classList.add('hidden'); | |
| toast.classList.remove('flex'); | |
| }, 3000); | |
| } | |
| // AI hints for specific fields | |
| function showAIHint(input) { | |
| // Could show contextual AI suggestions based on field type | |
| // Currently minimal to avoid intrusion | |
| } | |
| })(); |