/* Endoxa — arbejdsværktøj (prototype). Klinisk beslutningsstøtte, RAG-stil.
Pilotdomæne: hæmatologi · myelomatose & MGUS. */
const { useState, useRef, useEffect, useCallback } = React;
const KB = window.ENDOXA_KB;
/* ── Matchning af fri-tekst mod vidensgrundlaget ── */
function matchResponse(raw) {
const t = raw.toLowerCase();
// scenarie først
if (KB.scenario.keywords.some((k) => t.includes(k))) {
return { kind: "scenario", question: KB.scenario.question, answer: KB.scenario.answer };
}
// emner
for (const topic of KB.topics) {
if (topic.keywords.some((k) => t.includes(k))) {
return { kind: "topic", question: topic.question, answer: topic.answer };
}
}
// out of scope
if (KB.outOfScopeKeywords.some((k) => t.includes(k.trim()))) {
return { kind: "oos" };
}
return { kind: "fallback" };
}
/* ── Render af svar-blokke ── */
function Blocks({ blocks }) {
return blocks.map((b, i) =>
b.type === "para" ? (
{b.text}
) : (
{b.title}
{b.items.map((it, j) => {it} )}
)
);
}
/* ── Kilde-chips ── */
function Citations({ ids, onOpen }) {
if (!ids || !ids.length) return null;
return (
Kilder
{ids.map((id) => {
const s = KB.sources[id];
return (
onOpen(id)}>
{s.level}
{s.title}
);
})}
);
}
/* ── En AI-besked ── */
function AiMessage({ msg, onOpen, onAction, onFollowup }) {
if (msg.kind === "oos") {
return (
E
Uden for vidensgrundlaget
Det spørgsmål ligger uden for det aktuelle vidensgrundlag (hæmatologi: myelomatose og MGUS).
Jeg finder ikke på et svar her. Kontakt relevant instans — fx hæmatologisk bagvagt eller
den relevante specialrådgivning — eller vælg et hæmatologisk emne nedenfor.
);
}
if (msg.kind === "referral") {
const d = KB.referralDraft;
return (
E
{d.title}
{d.fields.map(([k, v], i) => (
{k}
{v}
))}
{d.note}
);
}
return (
E
{msg.note &&
{msg.note}
}
{msg.answer.followup && !msg.followAnswered && (
{msg.answer.followup.question}
{msg.answer.followup.options.map((o) => (
onFollowup(msg, o)}>
{o.label}
))}
)}
{msg.answer.actions && msg.answer.actions.includes("referral") && (
onAction("referral")}>
+ Generér henvisningsudkast
↑
↓
)}
);
}
/* ── Kilde-panel (skydeind) ── */
function SourcePanel({ id, onClose }) {
if (!id) return null;
const s = KB.sources[id];
return (
<>
Kilde i vidensgrundlaget
✕
{s.level}
{s.tag}
{s.title}
{s.org}
Citeret passage
{s.passage}
Endoxa svarer udelukkende ud fra kuraterede kilder som denne. Indholdet her er
illustrativt i prototypen.
>
);
}
/* ── Composer ── */
function Composer({ onSend, onImage }) {
const [text, setText] = useState("");
const [cpr, setCpr] = useState(false);
const taRef = useRef(null);
const check = (v) => { setText(v); setCpr(KB.cprRegex.test(v)); };
const redact = () => { const v = text.replace(KB.cprRegex, "●●●●●●-●●●●"); setText(v); setCpr(false); };
const submit = () => {
if (!text.trim() || cpr) return;
onSend(text.trim());
setText(""); setCpr(false);
};
const onKey = (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
};
useEffect(() => {
const ta = taRef.current; if (!ta) return;
ta.style.height = "auto"; ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
}, [text]);
return (
{cpr && (
!
Personhenførbare oplysninger registreret (CPR-mønster). Fjern dem, før du sender.
Fjern automatisk
)}
Ingen patientdata gemmes · CPR og navne afvises automatisk · Lægen har det sidste ord
);
}
/* ── Hurtigknapper ── */
function QuickRow({ onPick }) {
return (
{KB.starters.map((id) => {
const t = KB.topics.find((x) => x.id === id);
return onPick(t)}>{t.label} ;
})}
);
}
/* ── Velkomst ── */
function Welcome({ onPick }) {
return (
E
Hvad kan jeg hjælpe med?
Start en dialog på dit eget sprog og få svar, der bygger på gældende danske
vejledninger og forløbsbeskrivelser — med tydelig kildehenvisning.
Hyppige spørgsmål
{KB.topics.map((t) => (
onPick(t)}>{t.label}
))}
);
}
/* ── Sidebar ── */
function Sidebar() {
const convos = [
{ t: "M-komponent 18 g/L — MGUS?", active: true, time: "Nu" },
{ t: "Blodprøver ved knoglesmerter", time: "I dag" },
{ t: "Kontrolinterval lavrisiko-MGUS", time: "I går" },
{ t: "Henvisning ved anæmi + M-komp.", time: "2 dage" },
];
return (
Endoxa
+ Ny samtale
Seneste
{convos.map((c, i) => (
{c.t}
{c.time}
))}
Vidensgrundlag
5 godkendte kilder · hæmatologi
Vedligeholdt af klinisk styregruppe
);
}
/* ── Topbar ── */
function TopBar() {
return (
Hæmatologi
Myelomatose & MGUS
▾
Pilot · Region Nordjylland
JK
);
}
/* ── App ── */
function App() {
const [messages, setMessages] = useState([]);
const [sourceId, setSourceId] = useState(null);
const threadRef = useRef(null);
const push = useCallback((m) => setMessages((prev) => [...prev, m]), []);
useEffect(() => {
const el = threadRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [messages]);
const ask = useCallback((text) => {
push({ role: "user", text });
const r = matchResponse(text);
setTimeout(() => {
if (r.kind === "oos") push({ role: "ai", kind: "oos" });
else if (r.kind === "fallback")
push({
role: "ai",
note: "Jeg kunne ikke knytte spørgsmålet til en konkret kilde. Prøv at være mere specifik, eller vælg et emne:",
answer: { blocks: [], citations: [] },
});
else push({ role: "ai", answer: r.answer, id: Math.random() });
}, 240);
}, [push]);
const pickTopic = useCallback((topic) => {
push({ role: "user", text: topic.question });
setTimeout(() => push({ role: "ai", answer: topic.answer, id: Math.random() }), 240);
}, [push]);
const onImage = useCallback(() => {
push({ role: "user", image: "Laboratorieskema.jpg" });
setTimeout(() =>
push({
role: "ai",
note: "Billedet er modtaget og anonymiseret automatisk — ingen CPR eller navne gemmes. Jeg tolker ikke billeder i denne prototype, men her er de prøver, der typisk vurderes:",
answer: KB.topics.find((t) => t.id === "blodprover").answer,
}), 300);
}, [push]);
const onAction = useCallback((kind) => {
if (kind === "referral") setTimeout(() => push({ role: "ai", kind: "referral" }), 180);
}, [push]);
const onFollowup = useCallback((msg, opt) => {
setMessages((prev) => prev.map((m) => (m === msg ? { ...m, followAnswered: true } : m)));
push({ role: "user", text: opt.label });
const resp = KB.followupResponses[opt.responseId];
setTimeout(() => push({ role: "ai", answer: resp, id: Math.random() }), 240);
}, [push]);
const empty = messages.length === 0;
return (
Demo · indholdet er illustrativt og ikke fagligt valideret — ikke til klinisk brug. Indtast ikke patientidentificerbare data.
{empty ? (
) : (
messages.map((m, i) =>
m.role === "user" ? (
{m.image ? (
▦
{m.image}
anonymiseret ✓
) : m.text}
) : (
)
)
)}
{!empty && }
setSourceId(null)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );