Clinic — Secure Chat with Doctor
A secure patient-doctor chat: an encrypted header with the doctor's avatar and online status, message bubbles with timestamps and delivery ticks, quick-reply chips, an attachment button, and a typing indicator with a simulated doctor reply.
MCP
コード
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
/* ── Chat shell ── */
.chat {
width: 100%;
max-width: 460px;
height: min(720px, 92vh);
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Header ── */
.chat-head {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: linear-gradient(135deg, var(--teal-d), var(--teal-700));
color: #fff;
}
.back-btn {
border: none;
background: rgba(255, 255, 255, 0.16);
color: #fff;
width: 30px;
height: 30px;
border-radius: 9px;
font-size: 1.3rem;
line-height: 1;
cursor: pointer;
flex-shrink: 0;
transition: background 0.15s;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.28);
}
.doc-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--coral);
color: #fff;
font-weight: 700;
font-size: 0.86rem;
flex-shrink: 0;
}
.doc-avatar.sm {
width: 30px;
height: 30px;
font-size: 0.68rem;
}
.doc-info {
flex: 1;
min-width: 0;
}
.doc-name {
font-weight: 700;
font-size: 0.98rem;
letter-spacing: -0.01em;
}
.doc-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
opacity: 0.9;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6ee7b7;
box-shadow: 0 0 0 3px rgba(110, 231, 183, 0.25);
}
.head-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.enc-badge {
display: inline-flex;
align-items: center;
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
padding: 3px 9px;
font-size: 0.66rem;
font-weight: 600;
white-space: nowrap;
}
.icon-btn {
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
width: 38px;
height: 38px;
border-radius: 11px;
font-size: 1rem;
cursor: pointer;
flex-shrink: 0;
transition: background 0.15s, border-color 0.15s;
}
.icon-btn:hover {
background: var(--teal-50);
border-color: var(--teal);
}
.head-actions .icon-btn {
width: 34px;
height: 34px;
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.12);
font-size: 0.9rem;
}
.head-actions .icon-btn:hover {
background: rgba(255, 255, 255, 0.26);
border-color: rgba(255, 255, 255, 0.5);
}
/* ── Thread ── */
.thread {
flex: 1;
overflow-y: auto;
padding: 18px 16px 8px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--bg);
}
.day-divider {
text-align: center;
margin: 2px 0 6px;
}
.day-divider span {
display: inline-block;
background: var(--white);
border: 1px solid var(--line);
color: var(--muted);
font-size: 0.72rem;
font-weight: 600;
padding: 3px 12px;
border-radius: 999px;
}
.msg {
display: flex;
flex-direction: column;
max-width: 78%;
}
.msg.in {
align-self: flex-start;
align-items: flex-start;
}
.msg.out {
align-self: flex-end;
align-items: flex-end;
}
.bubble {
padding: 10px 14px;
font-size: 0.9rem;
line-height: 1.45;
border-radius: var(--r-md);
box-shadow: var(--shadow-1);
word-wrap: break-word;
}
.msg.in .bubble {
background: var(--white);
color: var(--ink);
border-bottom-left-radius: 4px;
}
.msg.out .bubble {
background: linear-gradient(135deg, var(--teal), var(--teal-d));
color: #fff;
border-bottom-right-radius: 4px;
}
.time {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
color: var(--muted);
margin-top: 4px;
padding: 0 4px;
}
.tick {
color: var(--teal);
font-size: 0.68rem;
letter-spacing: -1px;
}
/* ── Typing indicator ── */
.typing {
display: flex;
align-items: flex-end;
gap: 8px;
align-self: flex-start;
}
.typing[hidden] {
display: none;
}
.typing-bubble {
display: flex;
align-items: center;
gap: 4px;
background: var(--white);
border-bottom-left-radius: 4px;
padding: 12px 14px;
}
.typing-bubble .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--muted);
animation: blink 1.2s infinite ease-in-out;
}
.typing-bubble .dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-bubble .dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%,
60%,
100% {
opacity: 0.3;
transform: translateY(0);
}
30% {
opacity: 1;
transform: translateY(-3px);
}
}
/* ── Quick replies ── */
.quick-replies {
display: flex;
gap: 8px;
padding: 10px 16px;
overflow-x: auto;
border-top: 1px solid var(--line);
background: var(--white);
}
.chip {
border: 1px solid var(--teal);
background: var(--teal-50);
color: var(--teal-d);
border-radius: 999px;
padding: 7px 13px;
font: inherit;
font-weight: 600;
font-size: 0.8rem;
white-space: nowrap;
cursor: pointer;
transition: background 0.15s, transform 0.12s;
}
.chip:hover {
background: #d6ece9;
}
.chip:active {
transform: translateY(1px);
}
/* ── Composer ── */
.composer {
display: flex;
align-items: flex-end;
gap: 10px;
padding: 12px 16px;
border-top: 1px solid var(--line);
background: var(--white);
}
.input {
flex: 1;
resize: none;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 10px 14px;
font: inherit;
font-size: 0.9rem;
color: var(--ink);
background: var(--bg);
max-height: 120px;
line-height: 1.45;
transition: border-color 0.15s, background 0.15s;
}
.input:focus {
outline: none;
border-color: var(--teal);
background: var(--white);
}
.input::placeholder {
color: var(--muted);
}
.send-btn {
border: none;
background: linear-gradient(135deg, var(--teal), var(--teal-d));
color: #fff;
width: 42px;
height: 42px;
border-radius: 50%;
font-size: 1.05rem;
cursor: pointer;
flex-shrink: 0;
display: grid;
place-items: center;
transition: transform 0.12s, opacity 0.15s;
}
.send-btn:hover {
opacity: 0.92;
}
.send-btn:active {
transform: scale(0.94);
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 520px) {
body {
padding: 0;
}
.chat {
max-width: 100%;
height: 100vh;
border: none;
border-radius: 0;
}
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
// ── Elements ─────────────────────────────────────────────────────────────────
const thread = document.getElementById("thread");
const typing = document.getElementById("typing");
const composer = document.getElementById("composer");
const input = document.getElementById("input");
// ── Timestamp formatter ──────────────────────────────────────────────────────
// Illustrative: minutes advance from a fixed seed instead of the real clock.
let clockMinutes = 9 * 60 + 12; // 9:12 AM
function nextTime() {
clockMinutes += 1;
const h = Math.floor(clockMinutes / 60) % 24;
const m = clockMinutes % 60;
const period = h >= 12 ? "PM" : "AM";
const h12 = h % 12 === 0 ? 12 : h % 12;
return `${h12}:${String(m).padStart(2, "0")} ${period}`;
}
function scrollToBottom() {
thread.scrollTop = thread.scrollHeight;
}
// ── Append a message bubble ──────────────────────────────────────────────────
function addMessage(text, dir) {
const msg = document.createElement("div");
msg.className = `msg ${dir}`;
const bubble = document.createElement("div");
bubble.className = "bubble";
bubble.textContent = text;
const time = document.createElement("span");
time.className = "time";
time.textContent = nextTime();
if (dir === "out") {
const tick = document.createElement("span");
tick.className = "tick";
tick.setAttribute("aria-label", "Read");
tick.textContent = "✓✓";
time.append(" ", tick);
}
msg.append(bubble, time);
thread.insertBefore(msg, typing);
scrollToBottom();
}
// ── Simulated doctor reply ───────────────────────────────────────────────────
const REPLIES = [
"Thanks for letting me know. Keep me posted if anything changes.",
"Good to hear. I'll note that in your chart.",
"Of course — let me explain that a little more clearly.",
"Sounds good. We can review this again at your next visit.",
"I'd recommend booking a follow-up so we can monitor it.",
];
let replyIndex = 0;
function doctorReply(patientText) {
typing.hidden = false;
scrollToBottom();
let reply;
const t = patientText.toLowerCase();
if (t.includes("clarify") || t.includes("?")) {
reply = REPLIES[2];
} else if (t.includes("book")) {
reply = REPLIES[3];
} else {
reply = REPLIES[replyIndex % REPLIES.length];
replyIndex += 1;
}
setTimeout(() => {
typing.hidden = true;
addMessage(reply, "in");
}, 1600);
}
// ── Send flow ────────────────────────────────────────────────────────────────
function send(text) {
const value = text.trim();
if (!value) return;
addMessage(value, "out");
input.value = "";
input.style.height = "auto";
doctorReply(value);
}
composer.addEventListener("submit", (e) => {
e.preventDefault();
send(input.value);
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send(input.value);
}
});
input.addEventListener("input", () => {
input.style.height = "auto";
input.style.height = `${input.scrollHeight}px`;
});
// ── Quick replies ────────────────────────────────────────────────────────────
document.getElementById("quickReplies").addEventListener("click", (e) => {
const chip = e.target.closest(".chip");
if (!chip) return;
send(chip.dataset.reply);
});
// ── Attachment ───────────────────────────────────────────────────────────────
document.getElementById("attachBtn").addEventListener("click", () => {
showToast("Attachments are encrypted before upload.");
});
scrollToBottom();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Secure Chat · Northbridge Clinic</title>
</head>
<body>
<main class="chat" aria-label="Secure chat with your doctor">
<header class="chat-head">
<button class="back-btn" aria-label="Back to messages">‹</button>
<span class="doc-avatar" aria-hidden="true">LO</span>
<div class="doc-info">
<p class="doc-name">Dr. Lena Okafor</p>
<p class="doc-meta">
<span class="status-dot"></span> Online · Cardiology
</p>
</div>
<div class="head-actions">
<span class="enc-badge">🔒 End-to-end encrypted</span>
<button class="icon-btn" aria-label="Start video call">📹</button>
</div>
</header>
<section class="thread" id="thread" aria-live="polite" aria-label="Message history">
<p class="day-divider"><span>Today</span></p>
<div class="msg in">
<div class="bubble">
Good morning, Amara. I've reviewed your latest lipid panel — everything looks
within range.
</div>
<span class="time">9:02 AM</span>
</div>
<div class="msg out">
<div class="bubble">
That's a relief, thank you! Should I keep taking the atorvastatin at night?
</div>
<span class="time">9:05 AM <span class="tick" aria-label="Read">✓✓</span></span>
</div>
<div class="msg in">
<div class="bubble">
Yes, please continue 20mg nightly. Any side effects — muscle aches, fatigue?
</div>
<span class="time">9:07 AM</span>
</div>
<div class="msg out">
<div class="bubble">No aches at all. Feeling good lately.</div>
<span class="time">9:08 AM <span class="tick" aria-label="Read">✓✓</span></span>
</div>
<div class="typing" id="typing" role="status" aria-live="polite" hidden>
<span class="doc-avatar sm" aria-hidden="true">LO</span>
<div class="bubble typing-bubble" aria-label="Doctor is typing">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
</div>
</div>
</section>
<div class="quick-replies" id="quickReplies" aria-label="Quick replies">
<button class="chip" data-reply="Thank you, doctor.">Thank you, doctor</button>
<button class="chip" data-reply="Yes, that works for me.">Yes</button>
<button class="chip" data-reply="I'll book that appointment.">I'll book that</button>
<button class="chip" data-reply="Could you clarify that for me?">
Can you clarify?
</button>
</div>
<form class="composer" id="composer">
<button type="button" class="icon-btn" id="attachBtn" aria-label="Add attachment">
📎
</button>
<textarea
class="input"
id="input"
rows="1"
placeholder="Write a secure message…"
aria-label="Message"
></textarea>
<button type="submit" class="send-btn" id="sendBtn" aria-label="Send message">
➤
</button>
</form>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Secure Chat with Doctor
A calm, clinical-white messaging thread between a patient and their doctor. The header carries the doctor’s avatar, name and specialty, an online indicator, and a 🔒 end-to-end encrypted badge. The scrollable thread opens on a short seeded conversation under a “Today” divider: doctor messages sit left in soft grey, patient messages right in teal with timestamps and a delivery tick. Quick-reply chips above the composer send common phrases in one tap, the attachment button toasts, and sending a message clears the input, scrolls to the bottom, shows a typing indicator, then drops in a simulated doctor reply. All interactions are vanilla JS.
Illustrative UI only — not intended for real medical use.