Render markdown in questions (#184)

This commit is contained in:
Matt Rubens 2025-07-02 11:26:33 -04:00 committed by GitHub
parent 2d8211aea7
commit 0156265963
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 2 deletions

View file

@ -63,6 +63,7 @@
"react-markdown": "^10.1.0",
"react-use": "^17.6.0",
"recharts": "^2.15.3",
"rehype-sanitize": "^6.0.0",
"remark-breaks": "^4.0.0",
"require-in-the-middle": "^7.5.2",
"shiki": "^3.7.0",

View file

@ -1,6 +1,7 @@
import { useMemo, useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
import rehypeSanitize from 'rehype-sanitize';
import { Link2 } from 'lucide-react';
import { toast } from 'sonner';
@ -284,8 +285,17 @@ export const Messages = ({
{isQuestion && questionData ? (
<div className="space-y-4">
{questionData.question && (
<div className="text-sm leading-relaxed">
{questionData.question}
<div className="text-sm leading-relaxed markdown-prose">
<ReactMarkdown
remarkPlugins={[remarkBreaks]}
rehypePlugins={[rehypeSanitize]}
components={{
a: PlainTextLink,
code: CodeBlock,
}}
>
{questionData.question}
</ReactMarkdown>
</div>
)}
{questionData.suggestions &&
@ -314,6 +324,7 @@ export const Messages = ({
<div className="text-sm leading-relaxed markdown-prose">
<ReactMarkdown
remarkPlugins={[remarkBreaks]}
rehypePlugins={[rehypeSanitize]}
components={{
a: PlainTextLink,
code: CodeBlock,

View file

@ -94,4 +94,45 @@ describe('Messages Component - Newline Handling', () => {
expect(commandContainer).toBeInTheDocument();
expect(commandContainer.closest('.font-mono')).toBeInTheDocument();
});
it('should handle question data with newlines in the question text', () => {
const questionMessage: Message = {
id: '1',
orgId: null,
userId: 'test-user',
taskId: 'test-task',
text: JSON.stringify({
question:
'What would you like to do?\nPlease choose from the options below:\n\n1. Option A\n2. Option B',
suggest: ['Option A', 'Option B'],
}),
timestamp: Date.now(),
ts: Date.now(),
type: 'ask',
say: null,
ask: 'followup',
mode: 'code',
reasoning: null,
partial: null,
};
render(<Messages messages={[questionMessage]} />);
// Question text should be present and properly formatted
expect(screen.getByText(/What would you like to do/)).toBeInTheDocument();
expect(
screen.getByText(/Please choose from the options below/),
).toBeInTheDocument();
// Should find both the list items and suggestion buttons (multiple instances expected)
const optionAElements = screen.getAllByText(/Option A/);
const optionBElements = screen.getAllByText(/Option B/);
// Should have at least 2 instances of each option (one in markdown list, one in suggestion button)
expect(optionAElements.length).toBeGreaterThanOrEqual(2);
expect(optionBElements.length).toBeGreaterThanOrEqual(2);
// Verify the markdown rendered the numbered list properly
expect(screen.getByRole('list')).toBeInTheDocument();
});
});

20
pnpm-lock.yaml generated
View file

@ -299,6 +299,9 @@ importers:
recharts:
specifier: ^2.15.3
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
rehype-sanitize:
specifier: ^6.0.0
version: 6.0.0
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
@ -4376,6 +4379,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-sanitize@5.0.2:
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
@ -5906,6 +5912,9 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
rehype-sanitize@6.0.0:
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
remark-breaks@4.0.0:
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
@ -11064,6 +11073,12 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-sanitize@5.0.2:
dependencies:
'@types/hast': 3.0.4
'@ungap/structured-clone': 1.3.0
unist-util-position: 5.0.0
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
@ -12830,6 +12845,11 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
rehype-sanitize@6.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-sanitize: 5.0.2
remark-breaks@4.0.0:
dependencies:
'@types/mdast': 4.0.4