Spaces:
Running
Running
| const state = { | |
| mode: "idle", | |
| step: null, | |
| booking: {}, | |
| cancellationId: "", | |
| feedbackRating: null, | |
| }; | |
| const doctors = { | |
| Cardiology: ["Dr. Asha Patel", "Dr. Michael Reed"], | |
| Orthopedics: ["Dr. Daniel Brooks", "Dr. Priya Nair"], | |
| Dermatology: ["Dr. Sara Kim", "Dr. Omar Khan"], | |
| Pediatrics: ["Dr. Neha Shah", "Dr. Luis Moreno"], | |
| "Internal Medicine": ["Dr. Rachel Adams", "Dr. Vikram Rao"], | |
| }; | |
| const symptomDepartments = [ | |
| { terms: ["chest", "heart", "palpitation", "blood pressure"], department: "Cardiology" }, | |
| { terms: ["knee", "bone", "joint", "back", "fracture", "shoulder"], department: "Orthopedics" }, | |
| { terms: ["rash", "skin", "acne", "itch"], department: "Dermatology" }, | |
| { terms: ["child", "baby", "pediatric", "fever"], department: "Pediatrics" }, | |
| ]; | |
| const faqs = [ | |
| { | |
| terms: ["visiting", "hours"], | |
| answer: "General visiting hours are 9:00 AM to 7:00 PM. Intensive care and specialty units may use separate schedules.", | |
| }, | |
| { | |
| terms: ["document", "bring", "id", "insurance"], | |
| answer: "Please bring a photo ID, insurance card if applicable, referral letter if required, medication list, and previous reports.", | |
| }, | |
| { | |
| terms: ["emergency", "er", "urgent"], | |
| answer: "The emergency department is open 24/7. If this is a medical emergency, call emergency services or go to the nearest emergency department.", | |
| }, | |
| { | |
| terms: ["parking"], | |
| answer: "Patient parking is available near the main entrance. Accessible parking is located beside the reception entrance.", | |
| }, | |
| { | |
| terms: ["telehealth", "online", "video"], | |
| answer: "Telehealth appointments are available for selected departments. The care team confirms eligibility during booking.", | |
| }, | |
| ]; | |
| const chatLog = document.querySelector("[data-chat-log]"); | |
| const form = document.querySelector("[data-chat-form]"); | |
| const input = document.querySelector("[data-chat-input]"); | |
| const quickActions = document.querySelector("[data-quick-actions]"); | |
| const memoryPanel = document.querySelector("[data-memory]"); | |
| const appointmentPanel = document.querySelector("[data-appointments]"); | |
| const resetButton = document.querySelector("[data-reset]"); | |
| function getMemory() { | |
| return JSON.parse(localStorage.getItem("mediflow-memory") || "{}"); | |
| } | |
| function setMemory(nextMemory) { | |
| localStorage.setItem("mediflow-memory", JSON.stringify(nextMemory)); | |
| renderPanels(); | |
| } | |
| function getAppointments() { | |
| return JSON.parse(localStorage.getItem("mediflow-appointments") || "[]"); | |
| } | |
| function setAppointments(appointments) { | |
| localStorage.setItem("mediflow-appointments", JSON.stringify(appointments)); | |
| renderPanels(); | |
| } | |
| function addMessage(sender, text, options = {}) { | |
| const message = document.createElement("div"); | |
| message.className = `message ${sender}`; | |
| if (options.card) { | |
| message.classList.add("card-message"); | |
| } | |
| message.innerHTML = text; | |
| chatLog.appendChild(message); | |
| chatLog.scrollTop = chatLog.scrollHeight; | |
| } | |
| function addBot(text, options) { | |
| addMessage("bot", text, options); | |
| } | |
| function addUser(text) { | |
| addMessage("user", escapeHtml(text)); | |
| } | |
| function escapeHtml(value) { | |
| return value.replace(/[&<>"']/g, (char) => ({ | |
| "&": "&", | |
| "<": "<", | |
| ">": ">", | |
| '"': """, | |
| "'": "'", | |
| }[char])); | |
| } | |
| function renderQuickActions(actions = []) { | |
| quickActions.innerHTML = ""; | |
| actions.forEach((action) => { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "choice"; | |
| button.textContent = action.label; | |
| button.addEventListener("click", () => handleInput(action.value || action.label, true)); | |
| quickActions.appendChild(button); | |
| }); | |
| } | |
| function renderPanels() { | |
| const memory = getMemory(); | |
| const appointments = getAppointments(); | |
| memoryPanel.innerHTML = ` | |
| <dt>Name</dt><dd>${memory.fullName || "Not saved"}</dd> | |
| <dt>Department</dt><dd>${memory.preferredDepartment || "Not saved"}</dd> | |
| <dt>Doctor</dt><dd>${memory.preferredDoctor || "Not saved"}</dd> | |
| `; | |
| if (!appointments.length) { | |
| appointmentPanel.innerHTML = "<p>No active prototype appointments.</p>"; | |
| return; | |
| } | |
| appointmentPanel.innerHTML = appointments.map((appointment) => ` | |
| <article class="mini-card"> | |
| <strong>${appointment.id}</strong> | |
| <span>${appointment.doctor}</span> | |
| <span>${appointment.department} · ${appointment.date} · ${appointment.time}</span> | |
| </article> | |
| `).join(""); | |
| } | |
| function start() { | |
| const memory = getMemory(); | |
| const name = memory.fullName ? ` Welcome back, ${memory.fullName}.` : ""; | |
| addBot(`Hello, I am MediFlow Assistant.${name} I can help you book a doctor appointment, cancel an appointment, answer hospital questions, or collect feedback.`); | |
| renderQuickActions([ | |
| { label: "Book appointment" }, | |
| { label: "Cancel appointment" }, | |
| { label: "Ask hospital question" }, | |
| { label: "Give feedback" }, | |
| ]); | |
| renderPanels(); | |
| } | |
| function routeIntent(text) { | |
| const lower = text.toLowerCase(); | |
| if (lower.includes("cancel")) return beginCancellation(); | |
| if (lower.includes("feedback") || lower.includes("rate")) return beginFeedback(); | |
| if (lower.includes("hour") || lower.includes("insurance") || lower.includes("parking") || lower.includes("document") || lower.includes("emergency") || lower.includes("telehealth")) return answerFaq(text); | |
| return beginBooking(text); | |
| } | |
| function beginBooking(initialText = "") { | |
| const memory = getMemory(); | |
| state.mode = "booking"; | |
| state.step = memory.fullName ? "reuseName" : "name"; | |
| state.booking = {}; | |
| if (initialText) { | |
| const department = detectDepartment(initialText); | |
| if (department) { | |
| state.booking.department = department; | |
| state.booking.symptoms = initialText; | |
| } | |
| } | |
| if (memory.fullName) { | |
| addBot(`Should I book this appointment for ${memory.fullName}?`); | |
| renderQuickActions([{ label: "Yes, use saved name" }, { label: "No, enter another name" }]); | |
| return; | |
| } | |
| addBot("Please enter the patient's full name."); | |
| renderQuickActions([]); | |
| } | |
| function continueBooking(text) { | |
| const memory = getMemory(); | |
| if (state.step === "reuseName") { | |
| if (text.toLowerCase().startsWith("yes")) { | |
| state.booking.patientName = memory.fullName; | |
| state.step = "contact"; | |
| addBot("Please enter a phone number or email for appointment updates."); | |
| renderQuickActions([]); | |
| return; | |
| } | |
| state.step = "name"; | |
| addBot("Please enter the patient's full name."); | |
| renderQuickActions([]); | |
| return; | |
| } | |
| if (state.step === "name") { | |
| state.booking.patientName = text; | |
| setMemory({ ...memory, fullName: text }); | |
| state.step = "contact"; | |
| addBot("Please enter a phone number or email for appointment updates."); | |
| return; | |
| } | |
| if (state.step === "contact") { | |
| state.booking.contact = text; | |
| if (state.booking.department) { | |
| state.step = "doctor"; | |
| askDoctor(); | |
| return; | |
| } | |
| state.step = "department"; | |
| addBot("Which department, doctor, or symptom should I use for this appointment?"); | |
| renderQuickActions([ | |
| { label: "Cardiology" }, | |
| { label: "Orthopedics" }, | |
| { label: "Dermatology" }, | |
| { label: "Pediatrics" }, | |
| ]); | |
| return; | |
| } | |
| if (state.step === "department") { | |
| const department = detectDepartment(text); | |
| state.booking.symptoms = text; | |
| state.booking.department = department || "Internal Medicine"; | |
| setMemory({ ...getMemory(), preferredDepartment: state.booking.department }); | |
| state.step = "doctor"; | |
| askDoctor(); | |
| return; | |
| } | |
| if (state.step === "doctor") { | |
| state.booking.doctor = text === "No preference" ? doctors[state.booking.department][0] : text; | |
| setMemory({ ...getMemory(), preferredDoctor: state.booking.doctor }); | |
| state.step = "date"; | |
| addBot("What date would you prefer? Example: May 20, 2026."); | |
| renderQuickActions([]); | |
| return; | |
| } | |
| if (state.step === "date") { | |
| state.booking.date = text; | |
| state.step = "time"; | |
| addBot("What time would you prefer?"); | |
| renderQuickActions([ | |
| { label: "9:30 AM" }, | |
| { label: "11:00 AM" }, | |
| { label: "2:30 PM" }, | |
| { label: "4:00 PM" }, | |
| ]); | |
| return; | |
| } | |
| if (state.step === "time") { | |
| state.booking.time = text; | |
| state.step = "confirm"; | |
| addConfirmationPreview(); | |
| renderQuickActions([{ label: "Confirm" }, { label: "Change details" }, { label: "Cancel booking" }]); | |
| return; | |
| } | |
| if (state.step === "confirm") { | |
| if (text === "Confirm") return confirmBooking(); | |
| if (text === "Change details") return beginBooking(); | |
| state.mode = "idle"; | |
| state.step = null; | |
| addBot("No problem. I cancelled this draft booking."); | |
| renderQuickActions([{ label: "Book appointment" }, { label: "Ask hospital question" }]); | |
| } | |
| } | |
| function detectDepartment(text) { | |
| const lower = text.toLowerCase(); | |
| const exact = Object.keys(doctors).find((department) => lower.includes(department.toLowerCase())); | |
| if (exact) return exact; | |
| const mapped = symptomDepartments.find((item) => item.terms.some((term) => lower.includes(term))); | |
| return mapped?.department || null; | |
| } | |
| function askDoctor() { | |
| const department = state.booking.department; | |
| const options = [...doctors[department], "No preference"]; | |
| addBot(`I mapped that request to ${department}. Please choose a doctor.`); | |
| addBot(` | |
| <div class="doctor-card" aria-label="Doctor profile card"> | |
| <div class="avatar">${department.slice(0, 1)}</div> | |
| <div> | |
| <strong>${doctors[department][0]}</strong> | |
| <span>${department} · Available Mon/Wed/Fri</span> | |
| <small>Caption: Doctor profile card showing specialty and available appointment days.</small> | |
| </div> | |
| </div> | |
| `, { card: true }); | |
| renderQuickActions(options.map((label) => ({ label }))); | |
| } | |
| function addConfirmationPreview() { | |
| const booking = state.booking; | |
| addBot(` | |
| <div class="confirmation-card"> | |
| <small>Appointment draft</small> | |
| <strong>${booking.patientName}</strong> | |
| <span>${booking.department} with ${booking.doctor}</span> | |
| <span>${booking.date} at ${booking.time}</span> | |
| <em>Caption: Appointment confirmation card showing doctor, department, date, time, and patient name.</em> | |
| </div> | |
| `, { card: true }); | |
| } | |
| function confirmBooking() { | |
| const id = `MF-${Date.now().toString().slice(-6)}`; | |
| const appointment = { ...state.booking, id }; | |
| setAppointments([...getAppointments(), appointment]); | |
| state.mode = "idle"; | |
| state.step = null; | |
| addBot(`Your appointment is confirmed. Appointment ID: <strong>${id}</strong>.`); | |
| addBot("Would you like to leave feedback or ask another hospital question?"); | |
| renderQuickActions([{ label: "Give feedback" }, { label: "Ask hospital question" }, { label: "Book another appointment" }]); | |
| } | |
| function beginCancellation() { | |
| state.mode = "cancellation"; | |
| state.step = "identify"; | |
| addBot("Please enter your appointment ID."); | |
| renderQuickActions(getAppointments().map((appointment) => ({ label: appointment.id }))); | |
| } | |
| function continueCancellation(text) { | |
| const appointment = getAppointments().find((item) => item.id.toLowerCase() === text.toLowerCase()); | |
| if (!appointment) { | |
| addBot("I could not find that prototype appointment ID. Please check the ID or start a new booking."); | |
| renderQuickActions([{ label: "Book appointment" }, { label: "Cancel appointment" }]); | |
| state.mode = "idle"; | |
| return; | |
| } | |
| if (state.step === "identify") { | |
| state.cancellationId = appointment.id; | |
| state.step = "confirmCancel"; | |
| addBot(`I found ${appointment.id}: ${appointment.department} with ${appointment.doctor} on ${appointment.date} at ${appointment.time}. Do you want to cancel it?`); | |
| renderQuickActions([{ label: "Yes, cancel" }, { label: "No, keep appointment" }]); | |
| return; | |
| } | |
| } | |
| function finishCancellation(text) { | |
| if (text === "Yes, cancel") { | |
| setAppointments(getAppointments().filter((appointment) => appointment.id !== state.cancellationId)); | |
| addBot("Your appointment has been cancelled. You can book another appointment if needed."); | |
| } else { | |
| addBot("Okay, I kept the appointment active."); | |
| } | |
| state.mode = "idle"; | |
| state.step = null; | |
| renderQuickActions([{ label: "Book appointment" }, { label: "Give feedback" }]); | |
| } | |
| function answerFaq(text) { | |
| state.mode = "idle"; | |
| state.step = null; | |
| const lower = text.toLowerCase(); | |
| const match = faqs.find((faq) => faq.terms.some((term) => lower.includes(term))); | |
| addBot(match ? match.answer : "I can help with visiting hours, required documents, emergency care, parking, and telehealth."); | |
| renderQuickActions([ | |
| { label: "Visiting hours" }, | |
| { label: "Required documents" }, | |
| { label: "Emergency department" }, | |
| { label: "Book appointment" }, | |
| ]); | |
| } | |
| function beginFeedback() { | |
| state.mode = "feedback"; | |
| state.step = "rating"; | |
| addBot("How would you rate this experience from 1 to 5?"); | |
| renderQuickActions(["1", "2", "3", "4", "5"].map((label) => ({ label }))); | |
| } | |
| function continueFeedback(text) { | |
| if (state.step === "rating") { | |
| state.feedbackRating = text; | |
| state.step = "comment"; | |
| addBot("Please share one thing we could improve."); | |
| renderQuickActions([]); | |
| return; | |
| } | |
| addBot(`Thank you. I saved your prototype feedback rating as ${state.feedbackRating}.`); | |
| state.mode = "idle"; | |
| state.step = null; | |
| renderQuickActions([{ label: "Book appointment" }, { label: "Ask hospital question" }]); | |
| } | |
| function handleInput(rawText, fromQuickAction = false) { | |
| const text = rawText.trim(); | |
| if (!text) return; | |
| addUser(text); | |
| input.value = ""; | |
| if (text === "Book appointment" || text === "Book another appointment") return beginBooking(); | |
| if (text === "Cancel appointment") return beginCancellation(); | |
| if (text === "Ask hospital question") { | |
| state.mode = "faq"; | |
| addBot("What hospital question can I help with?"); | |
| renderQuickActions([{ label: "Visiting hours" }, { label: "Required documents" }, { label: "Parking" }, { label: "Telehealth" }]); | |
| return; | |
| } | |
| if (text === "Give feedback") return beginFeedback(); | |
| if (state.mode === "booking") return continueBooking(text); | |
| if (state.mode === "cancellation" && state.step === "confirmCancel") return finishCancellation(text); | |
| if (state.mode === "cancellation") return continueCancellation(text); | |
| if (state.mode === "feedback") return continueFeedback(text); | |
| if (state.mode === "faq" || fromQuickAction) return answerFaq(text); | |
| return routeIntent(text); | |
| } | |
| form.addEventListener("submit", (event) => { | |
| event.preventDefault(); | |
| handleInput(input.value); | |
| }); | |
| resetButton.addEventListener("click", () => { | |
| localStorage.removeItem("mediflow-memory"); | |
| localStorage.removeItem("mediflow-appointments"); | |
| chatLog.innerHTML = ""; | |
| state.mode = "idle"; | |
| state.step = null; | |
| state.booking = {}; | |
| start(); | |
| }); | |
| start(); | |