From 15eaf17a6102fa1b667388c55b6206e8b2c750a2 Mon Sep 17 00:00:00 2001
From: Adam Wright
Date: Mon, 21 Sep 2026 15:44:54 +0000
Subject: [PATCH 1/2] Say who the summary is sent to, before the reader sends
it
The disclosure sat under the finished summary. By the time a reader could read
"AI-generated", their analysis result had already left Reactome -- so what was
offered as a disclosure was a description of something that had happened. What
the reader is deciding is whether their result may go to a third party, and
that decision is made at the button.
So the note moves above the click, and it names the company. "AI-generated"
describes a technology; the question a person asks before pressing a button
that sends their data somewhere is *who gets it*. React-to-Me runs on
Reactome's own infrastructure but calls OpenAI to write the text, which makes
OpenAI a third party to the request even though the reader never talks to it.
The same note now opens on either side of the click, which is why the copy is
in the present tense throughout -- "was given" is a lie to somebody who has not
pressed the button yet. It states what is sent (the result's pathways, not the
uploaded identifiers) and deliberately claims nothing about retention or
training: that is the provider's contract to state, not ours to summarise from
memory, and a wrong reassurance is worse than silence. There is a test for that
silence, because it is the kind of sentence somebody adds to be helpful.
On the unmatched identifiers: the panel asks only for the aggregate tier and
should keep doing so. The result tab already lists every unmatched identifier
in a sortable table with a CSV download, so the disclosing tier would send a
reader's uploaded data to a model to tell them something they can already read
beside it.
Co-Authored-By: Claude Opus 5
---
.../analysis-summary.component.html | 48 ++++++++++++++-----
.../analysis-summary.component.scss | 12 +++++
.../analysis-summary.component.ts | 9 +++-
.../app/analysis-summary/panel-copy.spec.ts | 27 +++++++++--
.../src/app/analysis-summary/panel-copy.ts | 37 ++++++++++++--
5 files changed, 112 insertions(+), 21 deletions(-)
diff --git a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
index 6c5e3697..ee9ded27 100644
--- a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
+++ b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
@@ -6,6 +6,25 @@
who never read it and make the ones who do wait behind them. -->
@if (summarisable() && !summary.asking() && !summary.state() && !summary.challenge()) {
+
+
+ Sends this result to a third party to be summarised.
+
+
- This summary was written by a language model, not by a curator, and it has not been reviewed.
- {{ provenance() }}
-
-
- It can be wrong or incomplete. Check it against the pathways listed above, and in a publication cite those
- pathways and the Reactome release rather than this text.
-
-
- }
+
+ @if (howOpen()) {
+
+
{{ recipientNote() }}
+
{{ provenance() }}
+
+ A language model writes it, not a curator, and nobody reviews it before you see it. It can be wrong or
+ incomplete. Check it against the pathways it cites, and in a publication cite those pathways and the Reactome
+ release rather than this text.
+
+
}
@if (summary.expired()) {
diff --git a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.scss b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.scss
index 19af673f..972e6a49 100644
--- a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.scss
+++ b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.scss
@@ -183,3 +183,15 @@ h3 {
background: var(--tertiary-contrast-3);
color: white;
}
+
+// Sits under the button it qualifies, and reads as information rather than as a
+// warning: this is what pressing the button does, not a hazard notice.
+.pre-disclosure {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px 8px;
+ margin: 6px 0 0;
+ font-size: 0.8rem;
+ color: var(--tertiary-contrast-3);
+}
diff --git a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
index 25470386..e2fc2e76 100644
--- a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
+++ b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
@@ -10,7 +10,11 @@ import {
viewChild,
} from '@angular/core';
import { renderChallenge } from '../../../../website-angular/src/app/search/answer/turnstile';
-import { provenance as describeProvenance, waitingMessage as describeWait } from './panel-copy';
+import {
+ provenance as describeProvenance,
+ recipientNote as describeRecipient,
+ waitingMessage as describeWait,
+} from './panel-copy';
import { SummaryService } from './summary.service';
import { type AnalysisType } from './summary-stream';
@@ -171,6 +175,9 @@ export class AnalysisSummaryComponent {
/** Whether the "how this was made" note is open. */
readonly howOpen = signal(false);
+ /** @see recipientNote -- names the third party, which "AI" does not. */
+ readonly recipientNote = describeRecipient;
+
/** @see provenance -- reads the *applied* tier, never the requested one. */
readonly provenance = computed(() => describeProvenance(this.summary.applied()));
diff --git a/projects/pathway-browser/src/app/analysis-summary/panel-copy.spec.ts b/projects/pathway-browser/src/app/analysis-summary/panel-copy.spec.ts
index 209b8dae..d19a4d73 100644
--- a/projects/pathway-browser/src/app/analysis-summary/panel-copy.spec.ts
+++ b/projects/pathway-browser/src/app/analysis-summary/panel-copy.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
-import { provenance, waitingMessage } from './panel-copy';
+import { provenance, recipientNote, waitingMessage } from './panel-copy';
describe('what the wait says', () => {
it('claims nothing about the result before the service has said anything', () => {
@@ -24,7 +24,7 @@ describe('what the panel says the summary was made from', () => {
});
it('says they were not, for an aggregate summary', () => {
- expect(provenance('aggregate')).toContain('not your identifiers');
+ expect(provenance('aggregate')).toContain('not the identifiers you uploaded');
});
it('does not claim identifiers were sent when nothing has said so', () => {
@@ -32,6 +32,27 @@ describe('what the panel says the summary was made from', () => {
// the lookup fail gives the aggregate summary. Erring towards "we sent
// them" would tell a reader their identifiers left the browser when they
// did not, which is the worse of the two mistakes.
- expect(provenance(null)).toContain('not your identifiers');
+ expect(provenance(null)).toContain('not the identifiers you uploaded');
+ });
+});
+
+describe('naming the third party', () => {
+ it('names the company, because "AI" names a technology', () => {
+ // The reader deciding whether to press the button is deciding whether their
+ // analysis result may leave Reactome. "Generated by AI" does not answer
+ // that; a company name does.
+ expect(recipientNote()).toContain('OpenAI');
+ expect(recipientNote()).toContain('third party');
+ });
+
+ it('claims nothing about what the provider does with it afterwards', () => {
+ // Retention and training are the provider's contract to state, not ours to
+ // summarise from memory -- and a wrong reassurance is worse than silence.
+ expect(recipientNote()).not.toMatch(/train|retain|delete|stored|privacy/i);
+ });
+
+ it('is written in a tense that is true on both sides of the click', () => {
+ // The note opens before the summary is asked for as well as after.
+ expect(`${recipientNote()} ${provenance(null)}`).not.toMatch(/\bwas given\b|\bwere sent\b/);
});
});
diff --git a/projects/pathway-browser/src/app/analysis-summary/panel-copy.ts b/projects/pathway-browser/src/app/analysis-summary/panel-copy.ts
index f9b53c16..05be1be2 100644
--- a/projects/pathway-browser/src/app/analysis-summary/panel-copy.ts
+++ b/projects/pathway-browser/src/app/analysis-summary/panel-copy.ts
@@ -37,12 +37,39 @@ export function waitingMessage(started: boolean): string {
* reader's behalf, which is the one thing this sentence exists to be right
* about.
*
- * Null -- no `start` seen -- reads as the aggregate case, because that is what
- * is true of a summary nothing has told us otherwise about, and because the
- * error worth avoiding is claiming identifiers were sent when they were not.
+ * Null -- no `start` seen, which includes every reader who has not yet clicked
+ * -- reads as the aggregate case. That is what the panel always asks for, and
+ * the error worth avoiding is telling somebody their identifiers were sent when
+ * they were not.
*/
export function provenance(applied: Disclosure | null): string {
+ // Present tense, because this note opens on both sides of the click: "was
+ // given" is a lie to somebody who has not pressed the button yet.
return applied === 'identifiers'
- ? 'It was given your analysis result and the identifiers from it that Reactome could not match.'
- : 'It was given your analysis result — the pathways it found — and not your identifiers.';
+ ? 'What is sent: your analysis result, and the identifiers from it that Reactome could not match.'
+ : 'What is sent: your analysis result — the pathways it found — and not the identifiers you uploaded.';
+}
+
+/**
+ * Who receives the result, named rather than implied.
+ *
+ * "AI-generated" describes a technology; a reader deciding whether to press the
+ * button is deciding whether their analysis result may leave Reactome, and the
+ * answer to "to whom" is a company. React-to-Me runs on Reactome's own
+ * infrastructure but calls OpenAI's models to write the text, so OpenAI is the
+ * third party even though the reader never talks to it.
+ *
+ * Present tense, and true both before the click and after it: the reader can
+ * open this note either side of pressing the button.
+ *
+ * Deliberately makes no claim about what the provider does with the data
+ * afterwards -- retention, training -- because that is their contract to state
+ * and not ours to summarise from memory.
+ */
+export function recipientNote(): string {
+ return (
+ 'Summaries are written by React-to-Me, Reactome’s assistant. It runs on Reactome ' +
+ 'infrastructure but sends the text of your request to OpenAI, which generates the summary. ' +
+ 'That makes OpenAI a third party to this request.'
+ );
}
From 143f3a3576a4d7b90529dc6a535aea03c50444da Mon Sep 17 00:00:00 2001
From: Adam Wright
Date: Mon, 21 Sep 2026 16:20:28 +0000
Subject: [PATCH 2/2] Show the note only beside something it explains
`howOpen` outlives whatever opened it. A reader opens "what gets sent" beside
the button, clicks, and the request fails: the button block is gone because a
state is set, the summary block is gone because there is no prose, and the note
rendered anyway -- an explanation of what gets sent to OpenAI floating above an
error message, attached to nothing.
Found reviewing this branch as a merge candidate rather than while writing it,
which is the point of doing that separately.
The condition that decides whether the button is offered now has a name,
because two things need the same answer and a second copy of it is how the two
would drift apart.
Not covered by a test: it is template wiring, and this repo cannot render a
component with an external templateUrl under vitest -- vitest.config.ts says
why, and moving the predicate into panel-copy.ts would only test `a || b`.
Verified by reading the three states it can be in and by the build. Saying so
rather than implying the tests below cover it.
Co-Authored-By: Claude Opus 5
---
.../analysis-summary.component.html | 4 +--
.../analysis-summary.component.ts | 26 +++++++++++++++++++
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
index ee9ded27..45951c87 100644
--- a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
+++ b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.html
@@ -4,7 +4,7 @@
service, and a reader opens a result far more often than they want one
written about. Asking on every view would spend the budget on people
who never read it and make the ones who do wait behind them. -->
- @if (summarisable() && !summary.asking() && !summary.state() && !summary.challenge()) {
+ @if (offering()) {
- @if (howOpen()) {
+ @if (showHow()) {
{{ recipientNote() }}
{{ provenance() }}
diff --git a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
index e2fc2e76..d07698a7 100644
--- a/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
+++ b/projects/pathway-browser/src/app/analysis-summary/analysis-summary.component.ts
@@ -175,6 +175,32 @@ export class AnalysisSummaryComponent {
/** Whether the "how this was made" note is open. */
readonly howOpen = signal(false);
+ /**
+ * Whether the button that offers a summary is on screen.
+ *
+ * Its own name because two things need it: the button block, and the note
+ * below, which may only appear beside something it explains.
+ */
+ readonly offering = computed(
+ () =>
+ this.summarisable() &&
+ !this.summary.asking() &&
+ !this.summary.state() &&
+ !this.summary.challenge()
+ );
+
+ /**
+ * Whether the note has anything to sit under.
+ *
+ * `howOpen` survives the thing that opened it -- a reader opens it beside the
+ * button, asks, and the request fails -- and without this the note rendered
+ * on its own: an explanation of what gets sent, floating above an error,
+ * with no button and no summary anywhere near it.
+ */
+ readonly showHow = computed(
+ () => this.howOpen() && (this.offering() || (this.summary.visible() && !!this.heading()))
+ );
+
/** @see recipientNote -- names the third party, which "AI" does not. */
readonly recipientNote = describeRecipient;