Upload folder using huggingface_hub
Browse files
frontend/public/case-study/BlakeGilks.jpg
ADDED
|
frontend/public/case-study/assets/study.css
CHANGED
|
@@ -135,6 +135,7 @@ blockquote.pull p,
|
|
| 135 |
.cap-list,
|
| 136 |
.ref-list,
|
| 137 |
.ledge span,
|
|
|
|
| 138 |
.study-table caption,
|
| 139 |
.study-table th,
|
| 140 |
.study-table td {
|
|
@@ -684,32 +685,41 @@ blockquote.pull p {
|
|
| 684 |
color: var(--ink-meta);
|
| 685 |
}
|
| 686 |
|
| 687 |
-
/*
|
| 688 |
-
|
| 689 |
-
.
|
| 690 |
-
max-width:
|
| 691 |
margin: 6vh auto 0;
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
}
|
| 699 |
.tissue-figure img {
|
| 700 |
display: block;
|
| 701 |
width: 100%;
|
| 702 |
-
max-width: 300px;
|
| 703 |
height: auto;
|
| 704 |
-
margin: 0 auto;
|
| 705 |
}
|
| 706 |
.tissue-figure figcaption {
|
| 707 |
-
margin:
|
| 708 |
-
max-width: 44ch;
|
| 709 |
font-style: italic;
|
| 710 |
-
font-size:
|
| 711 |
-
line-height: 1.
|
| 712 |
-
color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
|
| 715 |
/* ------------------------------ In-prose lists ------------------------------ */
|
|
|
|
| 135 |
.cap-list,
|
| 136 |
.ref-list,
|
| 137 |
.ledge span,
|
| 138 |
+
.ledge strong,
|
| 139 |
.study-table caption,
|
| 140 |
.study-table th,
|
| 141 |
.study-table td {
|
|
|
|
| 685 |
color: var(--ink-meta);
|
| 686 |
}
|
| 687 |
|
| 688 |
+
/* Section 03 carries the light background itself (the paper biome), so the transparent specimen figure needs
|
| 689 |
+
no card. It floats into the column and the closing prose wraps around it. */
|
| 690 |
+
.stats-tissue {
|
| 691 |
+
max-width: 720px;
|
| 692 |
margin: 6vh auto 0;
|
| 693 |
+
overflow: hidden; /* contain the float so the wrapper spans the figure */
|
| 694 |
+
}
|
| 695 |
+
.tissue-figure {
|
| 696 |
+
float: right;
|
| 697 |
+
width: 248px;
|
| 698 |
+
margin: 4px 0 16px 34px;
|
| 699 |
}
|
| 700 |
.tissue-figure img {
|
| 701 |
display: block;
|
| 702 |
width: 100%;
|
|
|
|
| 703 |
height: auto;
|
|
|
|
| 704 |
}
|
| 705 |
.tissue-figure figcaption {
|
| 706 |
+
margin-top: 12px;
|
|
|
|
| 707 |
font-style: italic;
|
| 708 |
+
font-size: 11.5px;
|
| 709 |
+
line-height: 1.5;
|
| 710 |
+
color: var(--ink-meta);
|
| 711 |
+
}
|
| 712 |
+
.stats-tissue .stats-foot {
|
| 713 |
+
max-width: none;
|
| 714 |
+
margin: 0;
|
| 715 |
+
}
|
| 716 |
+
@media (max-width: 640px) {
|
| 717 |
+
.tissue-figure {
|
| 718 |
+
float: none;
|
| 719 |
+
width: 100%;
|
| 720 |
+
max-width: 300px;
|
| 721 |
+
margin: 0 auto 22px;
|
| 722 |
+
}
|
| 723 |
}
|
| 724 |
|
| 725 |
/* ------------------------------ In-prose lists ------------------------------ */
|
frontend/public/case-study/index.html
CHANGED
|
@@ -332,7 +332,7 @@
|
|
| 332 |
</div>
|
| 333 |
|
| 334 |
<!-- ============ 3. Section 03: when the definition changes (numbers on terraces) ============ -->
|
| 335 |
-
<section class="stats" data-biome="
|
| 336 |
<div class="stats-head">
|
| 337 |
<p class="eyebrow">03.</p>
|
| 338 |
<h2 class="stats-title">When the definition of the disease changes</h2>
|
|
@@ -356,19 +356,25 @@
|
|
| 356 |
<li class="ledge"><strong>42%</strong><span>of patients had mismatch-repair testing at diagnosis, coverage ranging 3.5% to 95.4% across centres (Thompson et al., 2022)</span></li>
|
| 357 |
<li class="ledge"><strong>3</strong><span>layers to the reconstruction problem</span></li>
|
| 358 |
</ol>
|
| 359 |
-
<
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
<
|
| 367 |
-
<
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
</section>
|
| 373 |
|
| 374 |
<!-- ==== 4. The three layers (descend-then-sideways rig, workflow biome) ==== -->
|
|
|
|
| 332 |
</div>
|
| 333 |
|
| 334 |
<!-- ============ 3. Section 03: when the definition changes (numbers on terraces) ============ -->
|
| 335 |
+
<section class="stats" data-biome="paper">
|
| 336 |
<div class="stats-head">
|
| 337 |
<p class="eyebrow">03.</p>
|
| 338 |
<h2 class="stats-title">When the definition of the disease changes</h2>
|
|
|
|
| 356 |
<li class="ledge"><strong>42%</strong><span>of patients had mismatch-repair testing at diagnosis, coverage ranging 3.5% to 95.4% across centres (Thompson et al., 2022)</span></li>
|
| 357 |
<li class="ledge"><strong>3</strong><span>layers to the reconstruction problem</span></li>
|
| 358 |
</ol>
|
| 359 |
+
<div class="stats-tissue">
|
| 360 |
+
<figure class="tissue-figure reveal">
|
| 361 |
+
<img src="./assets/papers/hjelmeland.png" alt="Vimentin immunohistochemistry of endometrial tumour tissue at three staining intensities" loading="lazy" />
|
| 362 |
+
<figcaption>
|
| 363 |
+
Vimentin immunohistochemistry on archived endometrial tumour tissue, graded by staining intensity.
|
| 364 |
+
Hjelmeland et al., BJC Reports 2, 81 (2024).
|
| 365 |
+
</figcaption>
|
| 366 |
+
</figure>
|
| 367 |
+
<p class="stats-foot">
|
| 368 |
+
A case recorded as stage IA under FIGO 2009 may need histologic review, lymphovascular space invasion
|
| 369 |
+
assessment, p53 immunohistochemistry, mismatch-repair testing, or POLE sequencing before a current
|
| 370 |
+
stage can be assigned. The historical cohort still helps: archives often preserve tissue blocks that
|
| 371 |
+
support later testing, so higher-resolution evidence can be generated years after diagnosis. The
|
| 372 |
+
practical work can be intensive: a team may need to retrieve reports, locate the corresponding tissue,
|
| 373 |
+
order new assays, identify which standard applies to each historical interpretation, join the new
|
| 374 |
+
results to the correct patient and specimen, and preserve why the final study value differs from the
|
| 375 |
+
original clinical label.
|
| 376 |
+
</p>
|
| 377 |
+
</div>
|
| 378 |
</section>
|
| 379 |
|
| 380 |
<!-- ==== 4. The three layers (descend-then-sideways rig, workflow biome) ==== -->
|
frontend/src/index.css
CHANGED
|
@@ -3199,6 +3199,8 @@ h2 {
|
|
| 3199 |
.mapping .mp-chat-form {
|
| 3200 |
display: flex;
|
| 3201 |
gap: 8px;
|
|
|
|
|
|
|
| 3202 |
}
|
| 3203 |
.mapping .mp-chat-form .mp-input {
|
| 3204 |
flex: 1;
|
|
@@ -3207,6 +3209,18 @@ h2 {
|
|
| 3207 |
color: #1c2b36;
|
| 3208 |
border: 1px solid var(--block-hairline);
|
| 3209 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3210 |
.mapping .mp-chat-form .mp-input::placeholder {
|
| 3211 |
color: #8a97a3;
|
| 3212 |
}
|
|
@@ -3963,10 +3977,23 @@ h2 {
|
|
| 3963 |
.cr .cr-chat-form {
|
| 3964 |
display: flex;
|
| 3965 |
gap: 8px;
|
|
|
|
|
|
|
| 3966 |
}
|
| 3967 |
.cr .cr-chat-form .cr-input {
|
| 3968 |
flex: 1;
|
| 3969 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3970 |
|
| 3971 |
/* --- Decision ledger --- */
|
| 3972 |
.cr .cr-ledger {
|
|
|
|
| 3199 |
.mapping .mp-chat-form {
|
| 3200 |
display: flex;
|
| 3201 |
gap: 8px;
|
| 3202 |
+
/* The input grows with its content; keep the Ask button pinned to the bottom edge as it does. */
|
| 3203 |
+
align-items: flex-end;
|
| 3204 |
}
|
| 3205 |
.mapping .mp-chat-form .mp-input {
|
| 3206 |
flex: 1;
|
|
|
|
| 3209 |
color: #1c2b36;
|
| 3210 |
border: 1px solid var(--block-hairline);
|
| 3211 |
}
|
| 3212 |
+
/* An auto-growing textarea: one line to start, grows with the message (see the useLayoutEffect that sets
|
| 3213 |
+
its height from scrollHeight), then scrolls past a cap so it never runs off the panel. */
|
| 3214 |
+
.mapping .mp-chat-form textarea.mp-input {
|
| 3215 |
+
height: auto;
|
| 3216 |
+
min-height: 36px;
|
| 3217 |
+
max-height: 132px;
|
| 3218 |
+
padding: 8px 10px;
|
| 3219 |
+
line-height: 1.4;
|
| 3220 |
+
resize: none;
|
| 3221 |
+
overflow-y: auto;
|
| 3222 |
+
box-sizing: border-box;
|
| 3223 |
+
}
|
| 3224 |
.mapping .mp-chat-form .mp-input::placeholder {
|
| 3225 |
color: #8a97a3;
|
| 3226 |
}
|
|
|
|
| 3977 |
.cr .cr-chat-form {
|
| 3978 |
display: flex;
|
| 3979 |
gap: 8px;
|
| 3980 |
+
/* Keep the Ask button at the bottom edge as the input grows with its content. */
|
| 3981 |
+
align-items: flex-end;
|
| 3982 |
}
|
| 3983 |
.cr .cr-chat-form .cr-input {
|
| 3984 |
flex: 1;
|
| 3985 |
}
|
| 3986 |
+
/* An auto-growing textarea (see the useLayoutEffect setting its height from scrollHeight): one line to
|
| 3987 |
+
start, grows with the message, then scrolls past a cap. */
|
| 3988 |
+
.cr .cr-chat-form textarea.cr-input {
|
| 3989 |
+
height: auto;
|
| 3990 |
+
min-height: 32px;
|
| 3991 |
+
max-height: 120px;
|
| 3992 |
+
line-height: 1.4;
|
| 3993 |
+
resize: none;
|
| 3994 |
+
overflow-y: auto;
|
| 3995 |
+
box-sizing: border-box;
|
| 3996 |
+
}
|
| 3997 |
|
| 3998 |
/* --- Decision ledger --- */
|
| 3999 |
.cr .cr-ledger {
|
frontend/src/pages/CaseReview.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
| 3 |
import {
|
| 4 |
askFieldChat,
|
|
@@ -1056,14 +1056,33 @@ function ValueChat({
|
|
| 1056 |
onFileChat: (r: FieldChatResponse, q: string) => void;
|
| 1057 |
}) {
|
| 1058 |
const [draft, setDraft] = useState("");
|
|
|
|
| 1059 |
|
| 1060 |
-
|
| 1061 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1062 |
const q = draft.trim();
|
| 1063 |
if (!q || busy) return;
|
| 1064 |
onAsk(q);
|
| 1065 |
setDraft("");
|
| 1066 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
|
| 1068 |
return (
|
| 1069 |
<div className="cr-chat">
|
|
@@ -1115,12 +1134,15 @@ function ValueChat({
|
|
| 1115 |
{busy && <p className="cr-chat-hint">Asking the model…</p>}
|
| 1116 |
</div>
|
| 1117 |
<form className="cr-chat-form" onSubmit={submit}>
|
| 1118 |
-
<
|
|
|
|
| 1119 |
className="cr-input"
|
|
|
|
| 1120 |
value={draft}
|
| 1121 |
placeholder={`e.g. why is ${block.label} ${formatValue(block.value) || "blank"}?`}
|
| 1122 |
aria-label="Ask about this value"
|
| 1123 |
onChange={(e) => setDraft(e.target.value)}
|
|
|
|
| 1124 |
/>
|
| 1125 |
<button type="submit" className="cr-btn-secondary" disabled={busy || !draft.trim()}>
|
| 1126 |
Ask
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
| 3 |
import {
|
| 4 |
askFieldChat,
|
|
|
|
| 1056 |
onFileChat: (r: FieldChatResponse, q: string) => void;
|
| 1057 |
}) {
|
| 1058 |
const [draft, setDraft] = useState("");
|
| 1059 |
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 1060 |
|
| 1061 |
+
// Grow the input with its content as a long question is typed, up to a cap where the field scrolls.
|
| 1062 |
+
useLayoutEffect(() => {
|
| 1063 |
+
const el = inputRef.current;
|
| 1064 |
+
if (!el) return;
|
| 1065 |
+
el.style.height = "auto";
|
| 1066 |
+
el.style.height = `${el.scrollHeight}px`;
|
| 1067 |
+
}, [draft]);
|
| 1068 |
+
|
| 1069 |
+
function doSubmit() {
|
| 1070 |
const q = draft.trim();
|
| 1071 |
if (!q || busy) return;
|
| 1072 |
onAsk(q);
|
| 1073 |
setDraft("");
|
| 1074 |
}
|
| 1075 |
+
function submit(e: React.FormEvent) {
|
| 1076 |
+
e.preventDefault();
|
| 1077 |
+
doSubmit();
|
| 1078 |
+
}
|
| 1079 |
+
function onInputKey(e: React.KeyboardEvent) {
|
| 1080 |
+
// Enter sends; Shift+Enter inserts a newline.
|
| 1081 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 1082 |
+
e.preventDefault();
|
| 1083 |
+
doSubmit();
|
| 1084 |
+
}
|
| 1085 |
+
}
|
| 1086 |
|
| 1087 |
return (
|
| 1088 |
<div className="cr-chat">
|
|
|
|
| 1134 |
{busy && <p className="cr-chat-hint">Asking the model…</p>}
|
| 1135 |
</div>
|
| 1136 |
<form className="cr-chat-form" onSubmit={submit}>
|
| 1137 |
+
<textarea
|
| 1138 |
+
ref={inputRef}
|
| 1139 |
className="cr-input"
|
| 1140 |
+
rows={1}
|
| 1141 |
value={draft}
|
| 1142 |
placeholder={`e.g. why is ${block.label} ${formatValue(block.value) || "blank"}?`}
|
| 1143 |
aria-label="Ask about this value"
|
| 1144 |
onChange={(e) => setDraft(e.target.value)}
|
| 1145 |
+
onKeyDown={onInputKey}
|
| 1146 |
/>
|
| 1147 |
<button type="submit" className="cr-btn-secondary" disabled={busy || !draft.trim()}>
|
| 1148 |
Ask
|
frontend/src/pages/Mapping.tsx
CHANGED
|
@@ -1097,14 +1097,34 @@ function EdgeChat({
|
|
| 1097 |
}) {
|
| 1098 |
const [draft, setDraft] = useState("");
|
| 1099 |
const identityReady = identity.name.trim() && identity.role.trim();
|
|
|
|
| 1100 |
|
| 1101 |
-
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
const q = draft.trim();
|
| 1104 |
if (!q || busy) return;
|
| 1105 |
askEdge(edge, q);
|
| 1106 |
setDraft("");
|
| 1107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
|
| 1109 |
function applyRemap(entry: Extract<ChatEntry, { kind: "assistant" }>) {
|
| 1110 |
const p = entry.response.proposal;
|
|
@@ -1191,12 +1211,15 @@ function EdgeChat({
|
|
| 1191 |
</p>
|
| 1192 |
)}
|
| 1193 |
<form className="mp-chat-form" onSubmit={submit}>
|
| 1194 |
-
<
|
|
|
|
| 1195 |
className="mp-input"
|
|
|
|
| 1196 |
value={draft}
|
| 1197 |
-
placeholder="e.g. why
|
| 1198 |
aria-label="Ask about this edge"
|
| 1199 |
onChange={(e) => setDraft(e.target.value)}
|
|
|
|
| 1200 |
/>
|
| 1201 |
<button type="submit" className="mp-btn-secondary" disabled={busy || !draft.trim()}>
|
| 1202 |
Ask
|
|
|
|
| 1097 |
}) {
|
| 1098 |
const [draft, setDraft] = useState("");
|
| 1099 |
const identityReady = identity.name.trim() && identity.role.trim();
|
| 1100 |
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 1101 |
|
| 1102 |
+
// Grow the input with its content, so a long question is fully visible as it is typed, up to a cap
|
| 1103 |
+
// where the field itself scrolls. Runs on every draft change, including the reset to "" after a send.
|
| 1104 |
+
useLayoutEffect(() => {
|
| 1105 |
+
const el = inputRef.current;
|
| 1106 |
+
if (!el) return;
|
| 1107 |
+
el.style.height = "auto";
|
| 1108 |
+
el.style.height = `${el.scrollHeight}px`;
|
| 1109 |
+
}, [draft]);
|
| 1110 |
+
|
| 1111 |
+
function doSubmit() {
|
| 1112 |
const q = draft.trim();
|
| 1113 |
if (!q || busy) return;
|
| 1114 |
askEdge(edge, q);
|
| 1115 |
setDraft("");
|
| 1116 |
}
|
| 1117 |
+
function submit(event: React.FormEvent) {
|
| 1118 |
+
event.preventDefault();
|
| 1119 |
+
doSubmit();
|
| 1120 |
+
}
|
| 1121 |
+
function onInputKey(event: React.KeyboardEvent) {
|
| 1122 |
+
// Enter sends; Shift+Enter inserts a newline, the usual chat convention.
|
| 1123 |
+
if (event.key === "Enter" && !event.shiftKey) {
|
| 1124 |
+
event.preventDefault();
|
| 1125 |
+
doSubmit();
|
| 1126 |
+
}
|
| 1127 |
+
}
|
| 1128 |
|
| 1129 |
function applyRemap(entry: Extract<ChatEntry, { kind: "assistant" }>) {
|
| 1130 |
const p = entry.response.proposal;
|
|
|
|
| 1211 |
</p>
|
| 1212 |
)}
|
| 1213 |
<form className="mp-chat-form" onSubmit={submit}>
|
| 1214 |
+
<textarea
|
| 1215 |
+
ref={inputRef}
|
| 1216 |
className="mp-input"
|
| 1217 |
+
rows={1}
|
| 1218 |
value={draft}
|
| 1219 |
+
placeholder="e.g. why this concept?"
|
| 1220 |
aria-label="Ask about this edge"
|
| 1221 |
onChange={(e) => setDraft(e.target.value)}
|
| 1222 |
+
onKeyDown={onInputKey}
|
| 1223 |
/>
|
| 1224 |
<button type="submit" className="mp-btn-secondary" disabled={busy || !draft.trim()}>
|
| 1225 |
Ask
|