fix(llm): stop CSS-injection clash on chat UI; style natively in HTI-5 palette
All checks were successful
CI / lint (push) Successful in 37s
CI / notebooks-smoke (push) Successful in 1m30s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 47s
Infra CI / zotero (push) Successful in 13s
Infra CI / docs (push) Successful in 16s
Infra CI / api (push) Successful in 1m3s
Infra CI / llm (push) Successful in 48s
Infra CI / mc (push) Successful in 12s
Deploy / report (push) Successful in 16s
CI / test (push) Successful in 14m45s
Harden / build-scan-report (push) Successful in 27m32s
Notebooks Integration / notebooks-integration (push) Successful in 8m1s
Zotero Sync / zotero-sync (push) Successful in 58s
Package Supply Chain / pkg-supply-chain (push) Successful in 59s

theme:true injected the generic fhirworx.css (light editorial, !important
rules) over the chat page's own dark theme, wrecking the colors. Set
theme:false and restyle the page natively in the platform palette
(cream/navy, Playfair/Source Serif), with cited [ID] tokens as mono chips.
This commit is contained in:
kert
2026-07-17 21:47:19 -04:00
parent 49d20ac949
commit 1cbecb110f
2 changed files with 110 additions and 36 deletions

View File

@@ -7,7 +7,7 @@
"zotero" (dict "port" "8080" "theme" true "mw" "git-sso,secure-headers") "zotero" (dict "port" "8080" "theme" true "mw" "git-sso,secure-headers")
"webdav" (dict "port" "8080" "theme" false "mw" "secure-headers") "webdav" (dict "port" "8080" "theme" false "mw" "secure-headers")
"api" (dict "port" "8000" "theme" false "mw" "secure-headers") "api" (dict "port" "8000" "theme" false "mw" "secure-headers")
"llm" (dict "port" "8000" "theme" true "mw" "git-sso,secure-headers") "llm" (dict "port" "8000" "theme" false "mw" "git-sso,secure-headers")
"nessie" (dict "port" "19120" "theme" false "mw" "git-sso,infra-headers") "nessie" (dict "port" "19120" "theme" false "mw" "git-sso,infra-headers")
"trino" (dict "port" "8080" "theme" true "mw" "git-sso,infra-headers") "trino" (dict "port" "8080" "theme" true "mw" "git-sso,infra-headers")
"polaris" (dict "port" "8181" "theme" false "mw" "git-sso,infra-headers") "polaris" (dict "port" "8181" "theme" false "mw" "git-sso,infra-headers")

View File

@@ -4,63 +4,121 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Comment Chat — fhirworx</title> <title>Comment Chat — fhirworx</title>
<link rel="icon" type="image/png" sizes="32x32" href="//dashboard.fhirworx.io/fav32.png">
<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=Playfair+Display:wght@600;700&family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@500&display=swap">
<style> <style>
/* HTI-5 editorial palette — matches the fhirworx platform theme. */
:root { :root {
--bg: #0f1115; --panel: #171a21; --line: #262b36; --fg: #e6e9ef; --background: #F7F5F0; --foreground: #1A1A18; --card: #FAFAF7;
--muted: #8b93a7; --accent: #6ea8fe; --user: #1f6feb22; --bot: #171a21; --primary: #1C2B3A; --primary-fg: #F7F5F0; --secondary: #EDEBE6;
--muted-fg: #6B6B68; --border: #D4D0C8; --accent: #2E3D8F;
--destructive: #C0392B;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; margin: 0; height: 100vh; display: flex; flex-direction: column;
color: var(--fg); background: var(--bg); height: 100vh; display: flex; background: var(--background); color: var(--foreground);
flex-direction: column; font-family: var(--font-body); font-size: 16px; line-height: 1.55;
} }
header { header {
padding: 12px 18px; border-bottom: 1px solid var(--line); flex: 0 0 auto; background: var(--primary); color: var(--primary-fg);
display: flex; align-items: baseline; gap: 12px; flex: 0 0 auto; border-bottom: 3px solid var(--foreground);
padding: 14px 22px; display: flex; align-items: baseline; gap: 14px;
}
header h1 {
font-family: var(--font-display); font-weight: 700; font-size: 20px; margin: 0;
}
header .sub { font-size: 13px; opacity: .7; }
header .who {
margin-left: auto; font-family: var(--font-mono); font-size: 11px;
text-transform: uppercase; letter-spacing: .08em; opacity: .8;
} }
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
header .who { color: var(--muted); font-size: 13px; margin-left: auto; }
#log { #log {
flex: 1 1 auto; overflow-y: auto; padding: 20px; display: flex; flex: 1 1 auto; overflow-y: auto; padding: 26px 22px;
flex-direction: column; gap: 16px; max-width: 820px; width: 100%; display: flex; flex-direction: column; gap: 22px;
margin: 0 auto; max-width: 860px; width: 100%; margin: 0 auto;
} }
.msg { display: flex; flex-direction: column; gap: 6px; } .msg { display: flex; flex-direction: column; gap: 7px; }
.msg .role { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; } .msg .role {
.bubble { padding: 12px 14px; border-radius: 10px; white-space: pre-wrap; word-wrap: break-word; } font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
.user .bubble { background: var(--user); align-self: flex-end; max-width: 80%; } text-transform: uppercase; color: var(--muted-fg);
.bot .bubble { background: var(--bot); border: 1px solid var(--line); } }
.sources { margin-top: 4px; font-size: 13px; } .bubble {
.sources summary { cursor: pointer; color: var(--muted); } padding: 14px 16px; white-space: pre-wrap; word-wrap: break-word;
.src { border-left: 2px solid var(--line); padding: 4px 10px; margin: 8px 0; color: var(--muted); } border: 1px solid var(--border); border-radius: 3px;
.src b { color: var(--accent); font-weight: 600; } }
.err { color: #ff7b72; } .user { align-items: flex-end; }
.user .role { text-align: right; }
.user .bubble {
background: var(--primary); color: var(--primary-fg);
border-color: var(--primary); max-width: 82%;
}
.bot .bubble { background: var(--card); }
/* Model-cited [ID] tokens rendered as editorial small-caps mono. */
.bot .bubble .cite {
font-family: var(--font-mono); font-size: .82em; color: var(--accent);
background: var(--secondary); padding: 1px 5px; border-radius: 2px;
white-space: nowrap;
}
details.sources { margin-top: 2px; font-size: 14px; }
details.sources > summary {
cursor: pointer; color: var(--muted-fg); font-family: var(--font-mono);
font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
list-style: none;
}
details.sources > summary::-webkit-details-marker { display: none; }
details.sources > summary::before { content: "▸ "; }
details.sources[open] > summary::before { content: "▾ "; }
.src {
border-left: 3px solid var(--border); padding: 6px 12px; margin: 10px 0;
color: var(--muted-fg);
}
.src b { color: var(--accent); font-family: var(--font-mono); font-size: .8em; font-weight: 500; }
.err { color: var(--destructive); border-color: var(--destructive); }
form { form {
flex: 0 0 auto; border-top: 1px solid var(--line); padding: 14px 18px; flex: 0 0 auto; border-top: 3px solid var(--foreground); background: var(--secondary);
display: flex; gap: 10px; max-width: 820px; width: 100%; margin: 0 auto; padding: 16px 22px; display: flex; gap: 12px;
max-width: 860px; width: 100%; margin: 0 auto;
} }
textarea { textarea {
flex: 1; resize: none; height: 44px; padding: 11px 12px; border-radius: 8px; flex: 1; resize: none; height: 46px; padding: 12px 13px;
border: 1px solid var(--line); background: var(--panel); color: var(--fg); font-family: var(--font-body); font-size: 16px;
font: inherit; background: var(--card); color: var(--foreground);
border: 1px solid var(--border); border-radius: 3px;
}
textarea:focus {
outline: none; border-color: var(--primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 22%, transparent);
} }
button { button {
padding: 0 18px; border: 0; border-radius: 8px; background: var(--accent); padding: 0 22px; border: 1px solid var(--primary); border-radius: 3px;
color: #06090f; font-weight: 600; cursor: pointer; background: var(--primary); color: var(--primary-fg);
font-family: var(--font-body); font-size: 15px; font-weight: 600; cursor: pointer;
transition: background .15s ease;
}
button:hover:not(:disabled) { background: #253748; }
button:disabled { opacity: .45; cursor: default; }
.hint {
margin: auto; max-width: 500px; text-align: center; color: var(--muted-fg);
font-style: italic;
} }
button:disabled { opacity: .5; cursor: default; }
.hint { color: var(--muted); text-align: center; margin: auto; max-width: 460px; }
</style> </style>
</head> </head>
<body> <body>
<header> <header>
<h1>Comment Chat</h1> <h1>Comment Chat</h1>
<span class="sub">grounded in indexed CMS rulemaking comments</span>
<span class="who" id="who"></span> <span class="who" id="who"></span>
</header> </header>
<div id="log"> <div id="log">
<div class="hint">Ask a question about the indexed CMS rulemaking comments. <div class="hint">Ask a question about the indexed CMS rulemaking comments.
Answers are grounded in the comments and cite their ids.</div> Answers are drawn only from the comments and cite their ids.</div>
</div> </div>
<form id="form"> <form id="form">
<textarea id="q" placeholder="e.g. What do commenters say about telehealth originating sites?" autofocus></textarea> <textarea id="q" placeholder="e.g. What do commenters say about telehealth originating sites?" autofocus></textarea>
@@ -74,7 +132,7 @@
const send = document.getElementById('send'); const send = document.getElementById('send');
fetch('whoami').then(r => r.json()).then(d => { fetch('whoami').then(r => r.json()).then(d => {
if (d.user) document.getElementById('who').textContent = 'signed in as ' + d.user; if (d.user) document.getElementById('who').textContent = d.user;
}).catch(() => {}); }).catch(() => {});
function bubble(role) { function bubble(role) {
@@ -91,6 +149,21 @@
return { wrap, b }; return { wrap, b };
} }
// Render answer text, wrapping [CMS-...] citations in styled spans.
function paint(el, text) {
el.innerHTML = '';
const re = /\[([A-Z0-9-]+)\]/g;
let last = 0, m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
const c = document.createElement('span');
c.className = 'cite'; c.textContent = m[0];
el.appendChild(c);
last = m.index + m[0].length;
}
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
}
function renderSources(wrap, sources) { function renderSources(wrap, sources) {
if (!sources || !sources.length) return; if (!sources || !sources.length) return;
const d = document.createElement('details'); const d = document.createElement('details');
@@ -116,6 +189,7 @@
if (hint) hint.remove(); if (hint) hint.remove();
bubble('user').b.textContent = question; bubble('user').b.textContent = question;
const { wrap, b } = bubble('bot'); const { wrap, b } = bubble('bot');
let answer = '';
send.disabled = true; send.disabled = true;
try { try {
const resp = await fetch('chat', { const resp = await fetch('chat', {
@@ -137,12 +211,12 @@
const line = part.replace(/^data: /, '').trim(); const line = part.replace(/^data: /, '').trim();
if (!line) continue; if (!line) continue;
const ev = JSON.parse(line); const ev = JSON.parse(line);
if (ev.type === 'token') { b.textContent += ev.text; log.scrollTop = log.scrollHeight; } if (ev.type === 'token') { answer += ev.text; paint(b, answer); log.scrollTop = log.scrollHeight; }
else if (ev.type === 'sources') renderSources(wrap, ev.sources); else if (ev.type === 'sources') renderSources(wrap, ev.sources);
else if (ev.type === 'error') { b.className = 'bubble err'; b.textContent = 'Error: ' + ev.message; } else if (ev.type === 'error') { b.className = 'bubble err'; b.textContent = 'Error: ' + ev.message; }
} }
} }
if (!b.textContent) b.textContent = '(no answer)'; if (!answer && b.className === 'bubble') b.textContent = '(no answer)';
} catch (e) { } catch (e) {
b.className = 'bubble err'; b.className = 'bubble err';
b.textContent = 'Error: ' + e.message; b.textContent = 'Error: ' + e.message;