Fix line breaks in AI output: use innerHTML instead of textContent

Adds setOutputText() helper that escapes HTML and converts \n to <br>,
applied to all AI-generated note outputs so line breaks are preserved
in contenteditable divs. Also cleans up CSS for quiz option markers.
This commit is contained in:
Daniel Onyejesi 2026-03-23 21:17:50 -04:00
parent 7eb72cee78
commit ca68177abb
10 changed files with 26 additions and 17 deletions

View file

@ -439,7 +439,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.shadess-flag-btn{font-size:11px;padding:2px 6px;}
/* Settings page */
.settings-page{display:flex;flex-direction:column;gap:0;max-width:800px;}
.settings-page{display:flex;flex-direction:column;gap:0;}
.settings-page .settings-section{padding:20px;margin-bottom:12px;border-radius:var(--radius);}
.settings-page .settings-section h3{font-size:16px;font-weight:700;color:var(--g800);margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--g100);display:flex;align-items:center;gap:8px;}
.settings-page .settings-section p{font-size:13px;color:var(--g600);margin-bottom:12px;line-height:1.6;}
@ -548,10 +548,7 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.lh-quiz-option input[type="radio"]{accent-color:var(--blue);width:18px;height:18px;flex-shrink:0;}
.lh-quiz-option span{flex:1;}
.lh-opt-correct{border-color:var(--green) !important;background:var(--green-light) !important;box-shadow:0 0 0 2px rgba(16,185,129,0.2) !important;}
.lh-opt-correct::after{content:'\\2713';position:absolute;right:14px;color:var(--green);font-weight:700;font-size:18px;}
.lh-quiz-option.lh-opt-correct,.lh-quiz-option.lh-opt-wrong{position:relative;}
.lh-opt-wrong{border-color:var(--red) !important;background:var(--red-light) !important;box-shadow:0 0 0 2px rgba(239,68,68,0.2) !important;}
.lh-opt-wrong::after{content:'\\2717';position:absolute;right:14px;color:var(--red);font-weight:700;font-size:18px;}
/* Quiz results */
.lh-result-item{margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--g100);}

View file

@ -223,6 +223,18 @@ document.addEventListener('DOMContentLoaded', function() {
// GLOBAL FUNCTIONS (must be outside DOMContentLoaded)
// ============================================================
// ── Set formatted text on output/contenteditable elements ──
// Converts \n to <br> so line breaks survive in contenteditable divs
// and are preserved when copying with innerText
function setOutputText(el, text) {
if (!el) return;
if (typeof el === 'string') el = document.getElementById(el);
if (!el) return;
// Escape HTML, then convert newlines to <br>
var safe = String(text || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
el.innerHTML = safe.replace(/\n/g, '<br>');
}
// ── Announcement banner ────────────────────────────────────
function loadAnnouncement() {
fetch('/api/admin/config/announcement', { headers: getAuthHeaders() })
@ -457,7 +469,7 @@ function refineDocument(outputElementId, inputElementId) {
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.success) { doc.textContent = data.refined; input.value = ''; showToast('Refined!', 'success'); }
if (data.success) { setOutputText(doc, data.refined); input.value = ''; showToast('Refined!', 'success'); }
else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
@ -476,7 +488,7 @@ function shortenDocument(outputElementId) {
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.success) { doc.textContent = data.shortened; showToast('Shortened!', 'success'); }
if (data.success) { setOutputText(doc, data.shortened); showToast('Shortened!', 'success'); }
else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });

View file

@ -141,7 +141,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
reviewText.textContent = data.review;
setOutputText(reviewText, data.review);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });

View file

@ -152,7 +152,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
courseText.textContent = data.hospitalCourse;
setOutputText(courseText, data.hospitalCourse);
formatTag.textContent = data.format || '';
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
@ -183,7 +183,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
clarifyOutput.textContent = data.questions;
setOutputText(clarifyOutput, data.questions);
clarifyOutput.classList.remove('hidden');
showToast('Review missing information below', 'info');
}

View file

@ -125,7 +125,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
hpiText.textContent = data.hpi;
setOutputText(hpiText, data.hpi);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });

View file

@ -130,7 +130,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
narrativeText.textContent = data.narrative;
setOutputText(narrativeText, data.narrative);
modelTag.textContent = (data.model || '').split('/').pop();
if (data.summary) {
summaryBar.innerHTML = '<div class="stat"><span class="dot dot-green"></span> Achieved: ' + data.summary.achieved + '</div>' +
@ -172,7 +172,7 @@
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-compress"></i> Regenerate Summary';
if (data.success) {
document.getElementById('ms-summary-text').textContent = data.summary;
setOutputText('ms-summary-text', data.summary);
document.getElementById('ms-quick-summary').classList.remove('hidden');
showToast('Summary generated!', 'success');
}

View file

@ -366,7 +366,7 @@
var out = document.getElementById('shadess-result-text');
var outCard = document.getElementById('shadess-output');
var tag = document.getElementById('shadess-model-tag');
if (out) out.textContent = data.assessment;
if (out) setOutputText(out, data.assessment);
if (tag) tag.textContent = (data.model || '').split('/').pop();
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
showToast('SSHADESS assessment generated', 'success');
@ -861,7 +861,7 @@
var noteEl = document.getElementById('wv-note-text');
var outCard = document.getElementById('wv-note-output');
var tag = document.getElementById('wv-note-model-tag');
if (noteEl) noteEl.textContent = data.note;
if (noteEl) setOutputText(noteEl, data.note);
if (tag) tag.textContent = (data.model || '').split('/').pop();
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
showToast('Well visit note generated!', 'success');

View file

@ -322,7 +322,7 @@
var noteEl = document.getElementById('sick-note-text');
var outCard = document.getElementById('sick-note-output');
var tag = document.getElementById('sick-note-model-tag');
if (noteEl) noteEl.textContent = data.note;
if (noteEl) setOutputText(noteEl, data.note);
if (tag) tag.textContent = (data.model || '').split('/').pop();
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
showToast('Sick visit note generated!', 'success');

View file

@ -82,7 +82,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
soapText.textContent = data.soap;
setOutputText(soapText, data.soap);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });

View file

@ -137,7 +137,7 @@
.then(function(data) {
hideLoading();
if (data.success) {
hpiText.textContent = data.hpi || data.soap;
setOutputText(hpiText, data.hpi || data.soap);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });