thors1's picture
🐳 12/04 - 12:18 - 🚨 SURGICAL FUNCTIONAL UPDATE — VERIFY MC ADD PATIENT FIX ONE-SENTENCE INTAKE FIELD + MAKE APP FULLY WORKING NON-NEGOTIABLE. THIS MUST BE A REAL INTERACTIVE WEB APP, NOT A STATIC UI
6b13557 verified
============== ---- One-Sentence Engine ----
function handleOneSentenceSubmit() {
const input = document.getElementById('one-sentence-input');
const engine = document.getElementById('one-sentence-engine');
if (!input || state.oneSentenceUsed) return;
const text = input.value.trim();
if (!text) return;
state.oneSentenceUsed = true;
// Parse the sentence
const parsed = parseOneSentence(text);
// Apply parsed data to state
Object.keys(parsed).forEach(key => {
if (parsed[key]) {
state.patientData[key] = parsed[key];
state.skippedFields.add(key);
}
});
// Update UI
updateRightPanel();
updateProgress();
// Visual feedback - fade the engine
if (engine) {
engine.classList.add('processed');
}
// Determine next step intelligently
const nextMissingField = findNextMissingField();
if (nextMissingField) {
// Skip to the missing field
const stepIndex = STEPS.findIndex(s => s.key === nextMissingField);
if (stepIndex !== -1) {
state.currentStep = stepIndex - 1;
// Show acknowledgment
setTimeout(() => {
appendAssistantMessage("Got it — I've filled most of this in.", null);
setTimeout(() => {
advanceStep();
}, 400);
}, 200);
return;
}
}
// If all filled or no match, show completion message
setTimeout(() => {
appendAssistantMessage("Got it — I've filled in everything I could find. Let me know if you'd like to add or change anything.", null);
setTimeout(() => {
showReviewStep();
}, 400);
}, 200);
}
function parseOneSentence(text) {
const result = {
fullName: '',
dob: '',
gender: '',
phone: '',
email: '',
address: '',
qualifyingCondition: '',
notes: ''
};
const lower = text.toLowerCase();
// Name extraction - look for capitalized words at start or common patterns
const nameMatch = text.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/) ||
text.match(/(?:name is|patient is|called)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)/i);
if (nameMatch) {
result.fullName = nameMatch[1].trim();
}
// Date of birth - various formats
const dobPatterns = [
/(?:born|dob|birth|birthdate)[\s:]?\s*(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/i,
/(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/,
/(?:born|dob)[\s:]?\s*([A-Za-z]+\s+\d{1,2},?\s+\d{4})/i
];
for (const pattern of dobPatterns) {
const match = text.match(pattern);
if (match) {
result.dob = formatDOB(match[1]);
break;
}
}
// Gender
const genderMatch = text.match(/\b(male|female|non-binary|non binary|man|woman)\b/i);
if (genderMatch) {
const g = genderMatch[1].toLowerCase();
result.gender = g === 'man' ? 'Male' : g === 'woman' ? 'Female' :
g === 'non binary' ? 'Non-binary' :
g.charAt(0).toUpperCase() + g.slice(1);
}
// Phone number
const phoneMatch = text.match(/(\(?\d{3}\)?[\s\-\.]?\d{3}[\s\-\.]?\d{4})/) ||
text.match(/(\d{3}[\s\-\.]\d{3}[\s\-\.]\d{4})/);
if (phoneMatch) {
result.phone = formatPhone(phoneMatch[1]);
}
// Email
const emailMatch = text.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);
if (emailMatch) {
result.email = emailMatch[1];
}
// Address - look for number + street patterns
const addressMatch = text.match(/(\d+\s+[A-Za-z\s]+(?:street|st|avenue|ave|road|rd|drive|dr|lane|ln|boulevard|blvd)[.,\s]+[A-Za-z\s]+(?:\d{5}(?:-\d{4})?)?)/i) ||
text.match(/(\d+\s+[A-Za-z\s]+,\s*[A-Za-z\s]+(?:,\s*[A-Za-z]{2})?\s*\d{5}(?:-\d{4})?)/i);
if (addressMatch) {
result.address = addressMatch[1].trim();
}
// Qualifying conditions
const conditionKeywords = {
'Chronic Pain': ['chronic pain', 'pain'],
'Anxiety': ['anxiety'],
'PTSD': ['ptsd', 'post traumatic', 'post-traumatic'],
'Cancer': ['cancer'],
'Arthritis': ['arthritis'],
'Severe Nausea': ['severe nausea', 'nausea'],
'Migraines': ['migraine', 'migraines'],
'Insomnia': ['insomnia', 'sleep'],
'Muscle Spasms': ['muscle spasm', 'muscle spasms', 'spasms'],
'Seizure Disorder': ['seizure', 'epilepsy'],
'HIV/AIDS': ['hiv', 'aids'],
'Glaucoma': ['glaucoma'],
'Neuropathy': ['neuropathy', 'nerve pain'],
'Chronic Inflammation': ['inflammation', 'inflammatory']
};
const foundConditions = [];
for (const [condition, keywords] of Object.entries(conditionKeywords)) {
for (const keyword of keywords) {
if (lower.includes(keyword)) {
foundConditions.push(condition);
break;
}
}
}
if (foundConditions.length > 0) {
result.qualifyingCondition = foundConditions.join(', ');
}
// Notes - anything after common delimiters that doesn't fit above
const noteIndicators = ['notes:', 'note:', 'additional', 'also', 'suffering from', 'has'];
for (const indicator of noteIndicators) {
const idx = lower.indexOf(indicator);
if (idx !== -1) {
const noteText = text.slice(idx + indicator.length).trim();
if (noteText && noteText.length > 3) {
result.notes = noteText;
break;
}
}
}
return result;
}
function findNextMissingField() {
// Find first step whose key is not in patientData or is empty
for (let i = 0; i < STEPS.length; i++) {
const step = STEPS[i];
const val = state.patientData[step.key];
if (!val || val === '' || val === 'Skipped' || val === 'Not provided') {
return step.key;
}
}
return null;
}
// ---- Utilities ----
function formatValue(key, value) {function startAIFlow() {
state.currentStep = -1;
// Wait briefly to see if user uses one-sentence input
setTimeout(() => {
if (!state.oneSentenceUsed && state.currentStep === -1) {
advanceStep();
}
}, 600);
}function setupEventListeners() {
sendBtn.addEventListener('click', handleSend);
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
skipBtn.addEventListener('click', handleSkip);
switchModeBtn.addEventListener('click', toggleMode);
saveDraftBtn.addEventListener('click', () => showToast('Draft saved'));
mobileRecordFab.addEventListener('click', openMobileSheet);
sheetOverlay.addEventListener('click', closeMobileSheet);
closeSheet.addEventListener('click', closeMobileSheet);
manualSaveDraft.addEventListener('click', () => showToast('Draft saved'));
manualCreatePatient.addEventListener('click', handleManualCreate);
// One-Sentence Engine listeners
const oneSentenceInput = document.getElementById('one-sentence-input');
const oneSentenceSend = document.getElementById('one-sentence-send');
if (oneSentenceInput && oneSentenceSend) {
oneSentenceInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleOneSentenceSubmit();
}
});
oneSentenceSend.addEventListener('click', handleOneSentenceSubmit);
}const state = {
mode: 'ai',
currentStep: -1,
patientData: {},
selectedConditions: new Set(),
isTyping: false,
flowComplete: false,
stepActive: false,
oneSentenceUsed: false,
skippedFields: new Set()
};// ============================================
// Verify MC — Add Patient — Application Logic
// ============================================
// ---- Constants ----
const CONDITIONS = [
'Chronic Pain', 'Anxiety', 'PTSD', 'Cancer', 'Arthritis',
'Severe Nausea', 'Migraines', 'Insomnia', 'Muscle Spasms',
'Seizure Disorder', 'HIV/AIDS', 'Glaucoma', 'Neuropathy',
'Chronic Inflammation', 'Other'
];
const STEPS = [
{
key: 'fullName',
question: "Let's begin the patient intake. What is the patient's full name?",
placeholder: "Type the patient's full name...",
type: 'text',
helper: "You can type naturally — I'll parse details from a sentence too."
},
{
key: 'dob',
question: "What is the patient's date of birth?",
placeholder: "e.g. 03/15/1990 or March 15, 1990",
type: 'text'
},
{
key: 'gender',
question: "What is the patient's gender?",
type: 'chips',
options: ['Male', 'Female', 'Non-binary', 'Prefer not to say']
},
{
key: 'phone',
question: "What's the best phone number for the patient?",
placeholder: "Type the patient's phone number...",
type: 'text'
},
{
key: 'email',
question: "And their email address?",
placeholder: "Type the patient's email address...",
type: 'text'
},
{
key: 'address',
question: "What is the patient's home address?",
placeholder: "Street, city, state, zip...",
type: 'text'
},
{
key: 'preferredContact',
question: "How should we prefer to contact the patient?",
type: 'chips',
options: ['Phone', 'Email', 'Text', 'No preference']
},
{
key: 'state',
question: "Which state is the patient located in?",
type: 'chips',
options: ['California', 'New York', 'Florida', 'Texas', 'Other']
},
{
key: 'qualifyingCondition',
question: "What qualifying condition(s) apply? Select all that apply.",
type: 'conditions',
note: 'Condition options may vary based on provider review and state guidance.'
},
{
key: 'medications',
question: "Is the patient currently taking any medications?",
placeholder: "List current medications, or type 'None'...",
type: 'text',
optional: true,
quickOptions: ['None', 'Unknown']
},
{
key: 'allergies',
question: "Does the patient have any known allergies?",
placeholder: "List allergies, or type 'None known'...",
type: 'text',
optional: true,
quickOptions: ['None known', 'Unknown']
},
{
key: 'pcp',
question: "Does the patient have a primary care physician?",
placeholder: "PCP name, or type 'None'...",
type: 'text',
optional: true,
quickOptions: ['None', 'Not provided']
},
{
key: 'insurance',
question: "What is the patient's insurance status?",
type: 'chips',
options: ['Insured', 'Uninsured', 'Self-pay', 'Prefer not to say']
},
{
key: 'emergencyContact',
question: "Emergency contact information?",
placeholder: "Contact name and phone number...",
type: 'text',
optional: true,
quickOptions: ['Not provided', 'Add later']
},
{
key: 'visitType',
question: "What type of visit is the patient scheduling?",
type: 'chips',
options: ['New Patient', 'Follow-up', 'Consultation', 'Renewal']
},
{
key: 'notes',
question: "Any additional intake notes?",
placeholder: "Add any relevant notes...",
type: 'text',
optional: true,
quickOptions: ['None', 'Add later']
}
];
const TOTAL_FIELDS = STEPS.length;
// ---- State ----
const state = {
mode: 'ai',
currentStep: -1,
patientData: {},
selectedConditions: new Set(),
isTyping: false,
flowComplete: false,
stepActive: false
};
// ---- DOM Refs ----
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const skipBtn = document.getElementById('skip-btn');
const quickOptions = document.getElementById('quick-options');
const switchModeBtn = document.getElementById('switch-mode-btn');
const saveDraftBtn = document.getElementById('save-draft-btn');
const progressBar = document.getElementById('progress-bar');
const progressPct = document.getElementById('progress-pct');
const aiLayout = document.getElementById('ai-layout');
const manualLayout = document.getElementById('manual-layout');
const liveBadge = document.getElementById('live-badge');
const pageDescription = document.getElementById('page-description');
const mobileRecordFab = document.getElementById('mobile-record-fab');
const mobileRecordSheet = document.getElementById('mobile-record-sheet');
const sheetOverlay = document.getElementById('sheet-overlay');
const closeSheet = document.getElementById('close-sheet');
const mobileRecordContent = document.getElementById('mobile-record-content');
const manualSaveDraft = document.getElementById('manual-save-draft');
const manualCreatePatient = document.getElementById('manual-create-patient');
// ---- Init ----
function init() {
lucide.createIcons();
setupEventListeners();
setTimeout(() => startAIFlow(), 400);
}
function startAIFlow() {
state.currentStep = -1;
advanceStep();
}
// ---- Event Listeners ----
function setupEventListeners() {
sendBtn.addEventListener('click', handleSend);
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
skipBtn.addEventListener('click', handleSkip);
switchModeBtn.addEventListener('click', toggleMode);
saveDraftBtn.addEventListener('click', () => showToast('Draft saved'));
mobileRecordFab.addEventListener('click', openMobileSheet);
sheetOverlay.addEventListener('click', closeMobileSheet);
closeSheet.addEventListener('click', closeMobileSheet);
manualSaveDraft.addEventListener('click', () => showToast('Draft saved'));
manualCreatePatient.addEventListener('click', handleManualCreate);
// Manual form field listeners
manualLayout.querySelectorAll('.manual-input').forEach(input => {
input.addEventListener('input', () => {
const field = input.dataset.field;
if (field) {
state.patientData[field] = input.value || '';
updateRightPanel();
updateProgress();
}
});
// Pre-fill from AI data
const field = input.dataset.field;
if (field && state.patientData[field]) {
input.value = state.patientData[field];
}
});
}
// ---- Chat Rendering ----
function appendAssistantMessage(text, extras) {
const wrapper = document.createElement('div');
wrapper.className = 'chat-msg flex items-start gap-2.5';
const avatar = document.createElement('div');
avatar.className = 'w-7 h-7 rounded-full bg-vmc-teal/8 border border-vmc-teal/15 flex items-center justify-center flex-shrink-0 mt-0.5';
avatar.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#0C8F8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>';
const bubble = document.createElement('div');
bubble.className = 'assistant-bubble flex-1 min-w-0';
const p = document.createElement('p');
p.textContent = text;
bubble.appendChild(p);
if (extras) {
extras(bubble);
}
wrapper.appendChild(avatar);
wrapper.appendChild(bubble);
chatMessages.appendChild(wrapper);
scrollToBottom();
}
function appendUserResponse(text) {
const wrapper = document.createElement('div');
wrapper.className = 'chat-msg flex justify-end';
const bubble = document.createElement('div');
bubble.className = 'user-bubble';
bubble.textContent = text;
wrapper.appendChild(bubble);
chatMessages.appendChild(wrapper);
scrollToBottom();
}
function appendChips(options, stepKey) {
const container = document.createElement('div');
container.className = 'chat-msg flex flex-wrap gap-2 mt-2 ml-[38px]';
options.forEach(opt => {
const chip = document.createElement('button');
chip.className = 'chip-option';
chip.textContent = opt;
chip.addEventListener('click', () => {
// Mark selected visually
container.querySelectorAll('.chip-option').forEach(c => c.classList.remove('selected'));
chip.classList.add('selected');
// Process answer
processAnswer(opt);
});
container.appendChild(chip);
});
chatMessages.appendChild(container);
scrollToBottom();
}
function appendConditionsGrid() {
const container = document.createElement('div');
container.className = 'chat-msg mt-2 ml-[38px]';
container.id = 'conditions-container';
const note = document.createElement('p');
note.className = 'text-[12px] text-vmc-muted mb-3';
note.textContent = 'Condition options may vary based on provider review and state guidance.';
container.appendChild(note);
const grid = document.createElement('div');
grid.className = 'grid grid-cols-2 sm:grid-cols-3 gap-2 mb-3';
CONDITIONS.forEach(cond => {
const card = document.createElement('button');
card.className = 'condition-card';
card.textContent = cond;
card.addEventListener('click', () => {
if (state.selectedConditions.has(cond)) {
state.selectedConditions.delete(cond);
card.classList.remove('selected');
} else {
state.selectedConditions.add(cond);
card.classList.add('selected');
}
updateConditionsContinue();
});
grid.appendChild(card);
});
container.appendChild(grid);
const continueRow = document.createElement('div');
continueRow.className = 'flex items-center gap-2';
const continueBtn = document.createElement('button');
continueBtn.className = 'primary-btn px-5 py-2 rounded-xl text-[13px] font-medium';
continueBtn.textContent = 'Continue';
continueBtn.id = 'conditions-continue';
continueBtn.disabled = true;
continueBtn.style.opacity = '0.5';
continueBtn.addEventListener('click', () => {
const selected = Array.from(state.selectedConditions);
if (selected.length > 0) {
processAnswer(selected.join(', '));
} else {
processAnswer('Not specified');
}
});
const skipLink = document.createElement('button');
skipLink.className = 'ghost-btn text-[12px] px-2 py-1 rounded';
skipLink.textContent = 'Skip for now';
skipLink.addEventListener('click', () => {
processAnswer('Not specified');
});
continueRow.appendChild(continueBtn);
continueRow.appendChild(skipLink);
container.appendChild(continueRow);
chatMessages.appendChild(container);
scrollToBottom();
}
function updateConditionsContinue() {
const btn = document.getElementById('conditions-continue');
if (btn) {
const hasSelection = state.selectedConditions.size > 0;
btn.disabled = !hasSelection;
btn.style.opacity = hasSelection ? '1' : '0.5';
}
}
function appendTypingIndicator() {
const wrapper = document.createElement('div');
wrapper.className = 'chat-msg flex items-start gap-2.5';
wrapper.id = 'typing-indicator';
const avatar = document.createElement('div');
avatar.className = 'w-7 h-7 rounded-full bg-vmc-teal/8 border border-vmc-teal/15 flex items-center justify-center flex-shrink-0 mt-0.5';
avatar.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#0C8F8B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>';
const dots = document.createElement('div');
dots.className = 'typing-indicator';
dots.innerHTML = '<span></span><span></span><span></span>';
wrapper.appendChild(avatar);
wrapper.appendChild(dots);
chatMessages.appendChild(wrapper);
scrollToBottom();
}
function removeTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) indicator.remove();
}
function appendReviewCard() {
const container = document.createElement('div');
container.className = 'chat-msg mt-2 ml-[38px]';
container.id = 'review-container';
const card = document.createElement('div');
card.className = 'review-card';
// Build sections
const sections = [
{
title: 'Personal Information',
fields: [
{ key: 'fullName', label: 'Full Name' },
{ key: 'dob', label: 'Date of Birth' },
{ key: 'gender', label: 'Gender' }
]
},
{
title: 'Contact Details',
fields: [
{ key: 'phone', label: 'Phone' },
{ key: 'email', label: 'Email' },
{ key: 'address', label: 'Address' },
{ key: 'preferredContact', label: 'Preferred Contact' }
]
},
{
title: 'Medical Information',
fields: [
{ key: 'qualifyingCondition', label: 'Condition' },
{ key: 'medications', label: 'Medications' },
{ key: 'allergies', label: 'Allergies' },
{ key: 'pcp', label: 'PCP' },
{ key: 'insurance', label: 'Insurance' }
]
},
{
title: 'Visit Details',
fields: [
{ key: 'visitType', label: 'Visit Type' },
{ key: 'emergencyContact', label: 'Emergency Contact' },
{ key: 'notes', label: 'Notes' }
]
}
];
sections.forEach((sec, sIdx) => {
const secDiv = document.createElement('div');
secDiv.className = 'review-section';
const secTitle = document.createElement('div');
secTitle.className = 'review-section-title';
secTitle.textContent = sec.title;
secDiv.appendChild(secTitle);
sec.fields.forEach(f => {
const row = document.createElement('div');
row.className = 'review-field';
const label = document.createElement('span');
label.className = 'review-field-label';
label.textContent = f.label;
const value = document.createElement('span');
const val = state.patientData[f.key];
value.className = 'review-field-value' + (val ? '' : ' empty');
value.textContent = val || 'Not provided';
row.appendChild(label);
row.appendChild(value);
secDiv.appendChild(row);
});
card.appendChild(secDiv);
});
container.appendChild(card);
// Action buttons
const actions = document.createElement('div');
actions.className = 'flex items-center gap-3 mt-4';
const createBtn = document.createElement('button');
createBtn.className = 'primary-btn px-6 py-2.5 rounded-xl text-[14px] font-medium';
createBtn.textContent = 'Create Patient';
createBtn.addEventListener('click', handleCreatePatient);
const draftBtn = document.createElement('button');
draftBtn.className = 'secondary-btn px-5 py-2.5 rounded-xl text-[14px] font-medium';
draftBtn.textContent = 'Save Draft';
draftBtn.addEventListener('click', () => showToast('Draft saved'));
actions.appendChild(createBtn);
actions.appendChild(draftBtn);
container.appendChild(actions);
chatMessages.appendChild(container);
scrollToBottom();
}
function appendSuccessState() {
const container = document.createElement('div');
container.className = 'chat-msg mt-4 flex justify-center';
const inner = document.createElement('div');
inner.className = 'success-state';
const check = document.createElement('div');
check.className = 'success-check';
check.innerHTML = '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#0C8F8B" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
const title = document.createElement('h3');
title.className = 'text-[17px] font-semibold text-vmc-text mb-1';
title.textContent = 'Patient created.';
const sub = document.createElement('p');
sub.className = 'text-[13px] text-vmc-muted';
const name = state.patientData.fullName || 'Patient';
sub.textContent = `${name} added to the system.`;
const doneBtn = document.createElement('button');
doneBtn.className = 'secondary-btn px-5 py-2 rounded-xl text-[13px] font-medium mt-5';
doneBtn.textContent = 'Done';
doneBtn.addEventListener('click', () => {
showToast('Returning to patients list...');
});
inner.appendChild(check);
inner.appendChild(title);
inner.appendChild(sub);
inner.appendChild(doneBtn);
container.appendChild(inner);
chatMessages.appendChild(container);
scrollToBottom();
}
// ---- Flow Control ----
function advanceStep() {
state.currentStep++;
state.stepActive = true;
if (state.currentStep >= STEPS.length) {
// Review step
showReviewStep();
return;
}
const step = STEPS[state.currentStep];
if (!step) return;
// ADAPTIVE FLOW: Check if this field is already filled
const existingValue = state.patientData[step.key];
if (existingValue && existingValue !== '' && existingValue !== 'Skipped' && existingValue !== 'Not provided') {
// Skip this step and move to next
state.stepActive = false;
advanceStep();
return;
}
state.isTyping = true;
disableComposer();
// Show typing indicator
appendTypingIndicator();
const delay = state.currentStep === 0 ? 500 : 400;
setTimeout(() => {
removeTypingIndicator();
state.isTyping = false;
// Show assistant message
appendAssistantMessage(step.question, step.helper ? ((bubble) => {
const helper = document.createElement('div');
helper.className = 'helper-text';
helper.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> ${step.helper}`;
bubble.appendChild(helper);
}) : null);
// Show interactive elements based on type
if (step.type === 'chips') {
appendChips(step.options, step.key);
updateComposerForStep(step);
} else if (step.type === 'conditions') {
appendConditionsGrid();
chatInput.placeholder = 'Or type a condition...';
chatInput.disabled = false;
sendBtn.disabled = false;
} else {
updateComposerForStep(step);
}
// Show skip for optional fields
if (step.optional) {
skipBtn.classList.remove('hidden');
} else {
skipBtn.classList.add('hidden');
}
// Show quick options
renderQuickOptions(step.quickOptions);
scrollToBottom();
}, delay);
}
function updateComposerForStep(step) {
if (step.placeholder) {
chatInput.placeholder = step.placeholder;
}
chatInput.disabled = false;
sendBtn.disabled = false;
chatInput.focus();
}
function disableComposer() {
chatInput.disabled = true;
sendBtn.disabled = true;
skipBtn.classList.add('hidden');
quickOptions.innerHTML = '';
}
function renderQuickOptions(options) {
quickOptions.innerHTML = '';
if (!options || options.length === 0) return;
options.forEach(opt => {
const btn = document.createElement('button');
btn.className = 'quick-option';
btn.textContent = opt;
btn.addEventListener('click', () => {
processAnswer(opt);
});
quickOptions.appendChild(btn);
});
}
function handleSend() {
if (state.isTyping || state.flowComplete) return;
const value = chatInput.value.trim();
if (!value) return;
chatInput.value = '';
processAnswer(value);
}
function handleSkip() {
if (state.isTyping || state.flowComplete) return;
const step = STEPS[state.currentStep];
if (!step) return;
processAnswer('Skipped');
}
function processAnswer(value) {
if (state.flowComplete) return;
const step = STEPS[state.currentStep];
if (!step) return;
state.stepActive = false;
// Clean up conditions container if present
const condContainer = document.getElementById('conditions-container');
if (condContainer) condContainer.remove();
// Store data
const formattedValue = formatValue(step.key, value);
state.patientData[step.key] = formattedValue;
// Show user response
appendUserResponse(formattedValue);
// Clear quick options
quickOptions.innerHTML = '';
// Disable composer briefly
disableComposer();
// Update right panel
updateRightPanel();
updateProgress();
// Advance to next step after a short natural delay
setTimeout(() => {
advanceStep();
}, 350);
}
function showReviewStep() {
state.isTyping = true;
disableComposer();
appendTypingIndicator();
setTimeout(() => {
removeTypingIndicator();
state.isTyping = false;
appendAssistantMessage("Please review and confirm.", null);
setTimeout(() => {
appendReviewCard();
chatInput.placeholder = 'Review above...';
chatInput.disabled = true;
sendBtn.disabled = true;
skipBtn.classList.add('hidden');
}, 180);
}, 500);
}
function handleCreatePatient() {
const reviewContainer = document.getElementById('review-container');
if (reviewContainer) reviewContainer.remove();
state.flowComplete = true;
disableComposer();
chatInput.placeholder = 'Patient intake complete';
liveBadge.style.display = 'none';
appendSuccessState();
updateProgress();
}
function handleManualCreate() {
// Gather all manual form data
manualLayout.querySelectorAll('.manual-input').forEach(input => {
const field = input.dataset.field;
if (field) {
state.patientData[field] = input.value || '';
}
});
updateRightPanel();
updateProgress();
showToast('Patient created successfully');
}
// ---- Right Panel Update ----
function updateRightPanel() {
const fieldRows = document.querySelectorAll('#record-panel .field-row, #mobile-record-content .field-row');
fieldRows.forEach(row => {
const key = row.dataset.field;
if (!key) return;
const valueEl = row.querySelector('.field-value');
if (!valueEl) return;
const val = state.patientData[key];
const oldVal = valueEl.textContent;
if (val && val !== 'Skipped' && val !== 'Not provided' && val !== 'Not specified') {
valueEl.textContent = val;
valueEl.className = 'field-value font-medium';
// Trigger update animation if value changed
if (oldVal !== val && oldVal !== '—') {
valueEl.classList.add('updated');
setTimeout(() => valueEl.classList.remove('updated'), 160);
}
} else if (val === 'Skipped') {
valueEl.textContent = 'Skipped';
valueEl.className = 'field-value skipped';
} else if (val === 'Not provided' || val === 'Not specified') {
valueEl.textContent = 'Not provided';
valueEl.className = 'field-value skipped';
} else {
valueEl.textContent = '—';
valueEl.className = 'field-value waiting';
}
});
// Update mobile record content
updateMobileRecordContent();
}
function updateProgress() {
const answered = Object.keys(state.patientData).filter(k => {
const v = state.patientData[k];
return v && v !== '' && v !== 'Skipped' && v !== 'Not provided' && v !== 'Not specified';
}).length;
const pct = Math.round((answered / TOTAL_FIELDS) * 100);
progressBar.style.width = pct + '%';
progressPct.textContent = pct + '%';
// Also update mobile if visible
const mobilePct = document.getElementById('mobile-progress-pct');
const mobileBar = document.getElementById('mobile-progress-bar');
if (mobilePct) mobilePct.textContent = pct + '%';
if (mobileBar) mobileBar.style.width = pct + '%';
}
// ---- Mobile Record Sheet ----
function updateMobileRecordContent() {
if (!mobileRecordContent) return;
const sections = [
{
title: 'Personal Information',
fields: [
{ key: 'fullName', label: 'Full Name' },
{ key: 'dob', label: 'Date of Birth' },
{ key: 'gender', label: 'Gender' }
]
},
{
title: 'Contact Details',
fields: [
{ key: 'phone', label: 'Phone' },
{ key: 'email', label: 'Email' },
{ key: 'address', label: 'Address' },
{ key: 'preferredContact', label: 'Preferred Contact' }
]
},
{
title: 'Medical Information',
fields: [
{ key: 'qualifyingCondition', label: 'Condition' },
{ key: 'medications', label: 'Medications' },
{ key: 'allergies', label: 'Allergies' },
{ key: 'pcp', label: 'PCP' },
{ key: 'insurance', label: 'Insurance' }
]
},
{
title: 'Visit Details',
fields: [
{ key: 'visitType', label: 'Visit Type' },
{ key: 'emergencyContact', label: 'Emergency Contact' },
{ key: 'notes', label: 'Notes' }
]
}
];
const answered = Object.keys(state.patientData).filter(k => {
const v = state.patientData[k];
return v && v !== '' && v !== 'Skipped' && v !== 'Not provided' && v !== 'Not specified';
}).length;
const pct = Math.round((answered / TOTAL_FIELDS) * 100);
let html = `
<div class="flex items-center justify-between mb-3">
<span class="text-[12px] font-medium text-vmc-muted">${pct}% complete</span>
</div>
<div class="w-full h-1.5 bg-gray-100 rounded-full mb-4 overflow-hidden">
<div id="mobile-progress-bar" class="h-full bg-vmc-teal rounded-full transition-all duration-300" style="width:${pct}%"></div>
</div>
`;
sections.forEach(sec => {
html += `<div class="mb-4">
<h3 class="text-[11px] font-medium uppercase tracking-wider text-vmc-disabled mb-2">${sec.title}</h3>
<div class="space-y-1.5">`;
sec.fields.forEach(f => {
const val = state.patientData[f.key];
let display = '—';
let cls = 'waiting';
if (val && val !== 'Skipped' && val !== 'Not provided' && val !== 'Not specified') {
display = val;
cls = '';
} else if (val === 'Skipped' || val === 'Not provided' || val === 'Not specified') {
display = 'Not provided';
cls = 'skipped';
}
html += `<div class="field-row" data-field="${f.key}">
<span class="field-label">${f.label}</span>
<span class="field-value ${cls}">${display}</span>
</div>`;
});
html += `</div></div>`;
});
mobileRecordContent.innerHTML = html;
}
function openMobileSheet() {
updateMobileRecordContent();
mobileRecordSheet.classList.remove('hidden');
requestAnimationFrame(() => {
const content = mobileRecordSheet.querySelector('.sheet-content');
content.style.transform = 'translateY(0)';
});
}
function closeMobileSheet() {
const content = mobileRecordSheet.querySelector('.sheet-content');
content.style.transform = 'translateY(100%)';
setTimeout(() => {
mobileRecordSheet.classList.add('hidden');
}, 300);
}
// ---- Mode Switching ----
function toggleMode() {
if (state.mode === 'ai') {
switchToManual();
} else {
switchToAI();
}
}
function switchToManual() {
state.mode = 'manual';
aiLayout.classList.add('hidden');
manualLayout.classList.remove('hidden');
switchModeBtn.innerHTML = '<i data-lucide="bot" class="w-3.5 h-3.5 mr-1.5"></i> Switch to AI Guided';
pageDescription.textContent = 'Manual entry mode';
liveBadge.style.display = 'none';
mobileRecordFab.style.display = 'none';
lucide.createIcons();
// Pre-fill manual form with existing data
manualLayout.querySelectorAll('.manual-input').forEach(input => {
const field = input.dataset.field;
if (field && state.patientData[field]) {
const val = state.patientData[field];
if (val !== 'Skipped' && val !== 'Not provided' && val !== 'Not specified') {
input.value = val;
}
}
});
}
function switchToAI() {
state.mode = 'ai';
manualLayout.classList.add('hidden');
aiLayout.classList.remove('hidden');
switchModeBtn.innerHTML = '<i data-lucide="pencil-line" class="w-3.5 h-3.5 mr-1.5"></i> Switch to Manual';
pageDescription.textContent = 'Guided by intelligent assistant';
if (!state.flowComplete) {
liveBadge.style.display = '';
}
lucide.createIcons();
}
// ---- Utilities ----
function formatValue(key, value) {
if (key === 'phone') {
return formatPhone(value);
}
if (key === 'dob') {
return formatDOB(value);
}
return value;
}
function formatPhone(val) {
const digits = val.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`;
}
if (digits.length === 11 && digits[0] === '1') {
return `(${digits.slice(1,4)}) ${digits.slice(4,7)}-${digits.slice(7)}`;
}
return val;
}
function formatDOB(val) {
// Try to parse common date formats
const d = new Date(val);
if (!isNaN(d.getTime())) {
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const yyyy = d.getFullYear();
return `${mm}/${dd}/${yyyy}`;
}
return val;
}
function scrollToBottom() {
requestAnimationFrame(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
});
}
function showToast(message) {
const toastInner = document.getElementById('toast-inner');
const toastText = document.getElementById('toast-text');
toastText.textContent = message;
toastInner.style.opacity = '1';
toastInner.style.transform = 'translateY(0)';
setTimeout(() => {
toastInner.style.opacity = '0';
toastInner.style.transform = 'translateY(8px)';
}, 2200);
}
// ---- Start ----
document.addEventListener('DOMContentLoaded', init);