thors1's picture
Initial DeepSite commit
ea75851 verified
// 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
}
})();