// encounter.jsx — Encounter screen wired to /api/sessions/{id}.
// The route param can be a session_id directly (from a visit-history click) or
// an appointment_id (from Dashboard / Schedule). For appointments we look up
// the linked session_id from the store; if there's no session yet, we show a
// "Start encounter" placeholder that routes the user to Transcribe.
const { useState: useStateE, useEffect: useEffectE, useRef: useRefE } = React;
function VitalChip({ label, value, unit }) {
return (
{label}
{value || "—"}
{unit && value && {unit} }
);
}
function copyClinicalNoteText(text, onCopied) {
const value = text || "";
const done = () => {
if (onCopied) onCopied();
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(value).then(done).catch(() => {
const el = document.createElement("textarea");
el.value = value;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
done();
});
return;
}
const el = document.createElement("textarea");
el.value = value;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
done();
}
function cleanNoteLine(line) {
return String(line || "")
.replace(/^#{1,6}\s+/, "")
.replace(/^[-*]\s+/, "")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/`([^`]+)`/g, "$1")
.trim();
}
function presentableNoteSections(text) {
const lines = (text || "").split(/\r?\n/);
const sections = [];
let current = { title: "Summary", items: [] };
const flush = () => {
if (current.items.length || current.title !== "Summary") sections.push(current);
};
lines.forEach(line => {
const trimmed = line.trim();
if (!trimmed) return;
if (/^#{1,6}\s+/.test(trimmed) || /^\*\*[^*]+:\*\*$/.test(trimmed) || /^[A-Z][A-Za-z /&-]{2,}:$/.test(trimmed)) {
flush();
current = { title: cleanNoteLine(trimmed).replace(/:$/, ""), items: [] };
return;
}
current.items.push(cleanNoteLine(trimmed));
});
flush();
return sections.length ? sections : [{ title: "Clinical note", items: [cleanNoteLine(text)] }];
}
function extractFollowupTextFromNote(text) {
const lines = String(text || "").split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const cleaned = cleanNoteLine(lines[i]).replace(/^follow[- ]?up\s*/i, "Follow-up").trim();
if (!/follow[- ]?up/i.test(cleaned)) continue;
if (cleaned.includes(":")) {
const value = cleaned.split(":").slice(1).join(":").trim();
if (value && !/^(not discussed|none|n\/?a)$/i.test(value)) return value;
}
for (let j = i + 1; j < Math.min(lines.length, i + 4); j++) {
const value = cleanNoteLine(lines[j]);
if (value && !/^(not discussed|none|n\/?a)$/i.test(value)) return value;
}
}
const match = String(text || "").match(/(follow[- ]?up[^.\n]*(?:in|on|within|after)\s+[^.\n]+)/i);
return match ? cleanNoteLine(match[1]) : "";
}
function followupTextFromSession(sessionData, noteText) {
const note = (sessionData && sessionData.clinical_note) || {};
const structured = note.structured_data || {};
const structuredFollow = structured && typeof structured === "object" ? String(structured.follow_up || "").trim() : "";
if (structuredFollow && !/^(not discussed|none|n\/?a)$/i.test(structuredFollow)) return structuredFollow;
const noteFollow = extractFollowupTextFromNote(noteText || note.note_text || "");
if (noteFollow) return noteFollow;
const transcriptText = ((sessionData && sessionData.transcripts) || [])
.map(seg => seg.corrected_text || seg.auto_corrected_text || seg.text || "")
.join("\n");
return extractFollowupTextFromNote(transcriptText);
}
function MarkdownPreview({ text, followupText }) {
const sections = presentableNoteSections(text);
const icons = [Icon.Clipboard, Icon.Pulse, Icon.Pill, Icon.CalendarPlus, Icon.Heart, Icon.Document];
return (
Clinical Note Preview
Formatted for review and printing
{followupText && (
)}
{sections.map((section, i) => {
const I = icons[i % icons.length];
return (
{section.title}
{section.items.map((item, j) => (
{item}
))}
);
})}
);
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function noteMarkdownToHtml(text) {
return presentableNoteSections(text).map(section => `
${escapeHtml(section.title)}
${section.items.map(item => `${escapeHtml(item)}
`).join("")}
`).join("");
}
function ClinicalNotePreviewModal({ open, noteText, patientName, followupText, onClose }) {
const [copied, setCopied] = useStateE(false);
if (!open) return null;
const title = patientName ? `Clinical note - ${patientName}` : "Clinical note";
function handleCopy() {
copyClinicalNoteText(noteText, () => {
setCopied(true);
setTimeout(() => setCopied(false), 1600);
});
}
function handlePrint() {
const printWindow = window.open("", "_blank", "width=900,height=720");
if (!printWindow) {
const printRoot = document.createElement("div");
printRoot.className = "ms-print-note-root";
printRoot.innerHTML = `${escapeHtml(title)} ${noteMarkdownToHtml(noteText)} `;
document.body.appendChild(printRoot);
document.body.classList.add("print-note-preview");
window.print();
setTimeout(() => {
document.body.classList.remove("print-note-preview");
printRoot.remove();
}, 300);
return;
}
printWindow.document.write(`${escapeHtml(title)} ${escapeHtml(title)} ${noteMarkdownToHtml(noteText)} `);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
return (
{title}
Formatted preview
{copied ? "Copied" : "Copy"}
Print
);
}
window.ClinicalNotePreviewModal = ClinicalNotePreviewModal;
window.copyClinicalNoteText = copyClinicalNoteText;
window.MSExtractFollowupText = extractFollowupTextFromNote;
window.MSFollowupTextFromSession = followupTextFromSession;
function Encounter({ appointmentId, onClose }) {
const store = window.MSStore.useStore();
const [session, setSession] = useStateE(null);
const [loading, setLoading] = useStateE(true);
const [error, setError] = useStateE(null);
const [noteText, setNoteText] = useStateE("");
const [savingNote, setSavingNote] = useStateE(false);
const [savedNote, setSavedNote] = useStateE(false);
const [appointment, setAppointment] = useStateE(null);
const [previewOpen, setPreviewOpen] = useStateE(false);
const [copiedNote, setCopiedNote] = useStateE(false);
// Resolve which session to load from the route param.
useEffectE(() => {
let cancelled = false;
setLoading(true);
setError(null);
setSession(null);
setAppointment(null);
setNoteText("");
(async () => {
// First, see if the param matches an appointment in the store.
const appt = (store.appointments || []).find(a => a.id === appointmentId);
if (appt) setAppointment(appt);
const candidateSessionId = appt && appt.sessionId ? appt.sessionId : appointmentId;
try {
const data = await store.getSession(candidateSessionId);
if (cancelled) return;
setSession(data);
setNoteText((data && data.clinical_note && data.clinical_note.note_text) || "");
setLoading(false);
} catch (e) {
if (cancelled) return;
setError(e && e.message || "Could not load encounter");
setLoading(false);
}
})();
return () => { cancelled = true; };
}, [appointmentId, store.appointments]);
async function saveNote() {
if (!session || !session.id || !noteText.trim() || savingNote) return;
setSavingNote(true);
try {
await store.saveClinicalNote(session.id, noteText);
setSavedNote(true);
setTimeout(() => setSavedNote(false), 1800);
} catch (e) {
// Surfaced via error state if needed.
} finally {
setSavingNote(false);
}
}
function copyNote() {
if (!noteText.trim()) return;
window.copyClinicalNoteText(noteText, () => {
setCopiedNote(true);
setTimeout(() => setCopiedNote(false), 1600);
});
}
// Loading
if (loading) {
return (
);
}
// No session yet — appointment-only path
if (error || !session) {
const patientName = appointment ? appointment.patientName : null;
return (
Back
{patientName && (
{(patientName || "?").split(" ").map(s => s[0]).slice(0, 2).join("").toUpperCase()}
{patientName}
{appointment &&
{appointment.type} · {appointment.duration}m · {appointment.time}
}
)}
No recorded session yet
{error ? error : "Start a transcription session from the Transcribe tab to capture this visit."}
);
}
// Real session view ───────────────────────────────────────────────────────
const patientName = session.patient_name || (appointment && appointment.patientName) || "Patient";
const initials = (patientName || "?").split(" ").map(s => s[0]).slice(0, 2).join("").toUpperCase();
const transcripts = session.transcripts || [];
const note = session.clinical_note;
const followupText = window.MSFollowupTextFromSession ? window.MSFollowupTextFromSession(session, noteText) : "";
const startedAt = session.started_at ? new Date(session.started_at) : null;
const duration = session.duration_seconds ? `${Math.round(session.duration_seconds / 60)}m` : "";
return (
{/* Top patient banner */}
Back
{initials}
{patientName}
{session.patient_age && · {session.patient_age} }
{note && Note generated }
{!note && Awaiting note }
{startedAt ? startedAt.toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" }) : ""}
{duration &&
{duration} session
}
{/* Left rail */}
{note && note.structured_data && (note.structured_data.diagnoses || []).length > 0 && (
Diagnoses
{note.structured_data.diagnoses.map((d, i) => (
{d}
))}
)}
{note && note.structured_data && (note.structured_data.medications_prescribed || []).length > 0 && (
Medications
{note.structured_data.medications_prescribed.map((m, i) => (
{typeof m === "string" ? m : m.name}
{typeof m !== "string" && m.dose && {m.dose} }
))}
)}
{/* Main content */}
{savingNote ? "Saving…" : savedNote ? "Saved." : note && note.created_at ? `Generated ${new Date(note.created_at).toLocaleString()}` : ""}
{/* Clinical note */}
N
Clinical note
setPreviewOpen(true)}>
Preview
{copiedNote ? "Copied" : "Copy"}
{savingNote ? "Saving…" : "Save"}
{/* Transcript */}
Transcript
{transcripts.length} segment{transcripts.length === 1 ? "" : "s"}
{transcripts.length === 0 && (
No transcript captured
)}
{transcripts.map((seg, i) => {
const speaker = seg.speaker_label || (i % 2 === 0 ? "Provider" : "Patient");
const isProvider = String(speaker).toLowerCase().includes("provider") || String(speaker) === "0";
const text = seg.corrected_text || seg.auto_corrected_text || seg.text || "";
return (
{isProvider ? "Dr" : initials}
{isProvider ? "Provider" : "Patient"}
{text}
);
})}
{/* Bottom action bar */}
Close
{savingNote ? "Saving…" : "Save & close"}
setPreviewOpen(false)}
/>
);
}
window.Encounter = Encounter;