Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Render the Ask answer as Markdown - #31

Closed
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render
Closed

Render the Ask answer as Markdown#31
Darkest-Teddy wants to merge 2 commits into
feat/product-in-the-atmospherefrom
ask-markdown-render

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

The Ask page was showing the model's answer as one raw blob - literal ### and ** on screen, no paragraphs, no bullets (see the reported screenshot).

This is a cherry-pick of b84728c from main, which fixed it there but never reached feat/product-in-the-atmosphere. The two branches had diverged; pages.tsx, services/api/ask.ts and test/pages.test.tsx were byte-identical to the commit's parent, and only app.css needed an auto-merge.

Both halves ship together, because neither works alone:

  • services/api/ask.ts asks the model for structure. A measured summary off the deployment was 5,953 characters with zero newlines - there was nothing to render, so the old <p> was not wrong.
  • apps/deliberation/src/markdown.tsx renders it: headings, bullets, ordered lists, **bold**, `code`.

The renderer builds React elements and never touches innerHTML, so HTML in a model's answer stays text on the page. Links are not a construct - an answer is drawn from a PDF page and has nowhere legitimate to point; provenance stays the citation rows the server resolves.

The prompt also forbids a marker between a number and its unit, so 300 mg/kg stays intact for ask-eval's 30[06]\s*mg/kg patterns.

The commit also carries the Ask-page fixes it was authored with: the source useState initialiser that left source pinned at "" (making every chip and the Ask button inert), and the picker/composer layout.

Verification

  • 939 tests pass across 63 files (npx vitest run), including 19 new markdown tests
  • npm run build --workspace @arbiter/deliberation clean
  • Confirmed the .md-h / .md-list styles survived the app.css auto-merge

🤖 Generated with Claude Code

THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b84728c)
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a3a6d3b-be4e-4526-8573-51d463b091c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Superseded by #32, which merges main into this branch and carries this work plus the run-on renderer fix.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230