Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 348 additions & 12 deletions components/__tests__/markdown.test.tsx

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions components/canvas/__tests__/canvas-code-block-newlines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';

import { markdownToHtml } from '@/lib/utils';

// Regression for issue #2109: ProseMirror's DOM parser drops newline-only
// text nodes between inline <span>s inside <pre>, so highlighted HTML merged
// code lines ("Dictfrom", "log_executiondef") once loaded via setContent.

const ARTIFACT_FENCE = [
'```python',
'import json',
'from typing import List, Dict',
'from functools import wraps',
'import logging',
'',
'logger = logging.getLogger(__name__)',
'',
'def log_execution(func):',
' """Decorator to log function execution times and results."""',
' @wraps(func)',
' def wrapper(*args, **kwargs):',
' logger.info(f"Starting {func.__name__}")',
' result = func(*args, **kwargs)',
' logger.info(f"Completed {func.__name__}")',
' return result',
' return wrapper',
'',
'@log_execution',
'def process_data_batch(input_records: List[Dict]) -> List[Dict]:',
' """Process a batch of input records and return transformed results."""',
' processed_results = []',
' for record in input_records:',
' transformed_record = {',
' "id": record.get("id"),',
' "value": record.get("amount", 0) * 1.1',
' }',
' processed_results.append(transformed_record)',
' return processed_results',
'```',
].join('\n');

const createEditor = (content: string) =>
new Editor({
extensions: [
StarterKit.configure({
heading: false,
codeBlock: {
HTMLAttributes: {
class: 'bg-muted rounded-md p-4 font-mono text-sm',
},
},
}),
],
content,
});

describe('canvas code block newline preservation (issue #2109)', () => {
it('keeps every code line separate after markdownToHtml + setContent', () => {
const editor = createEditor(markdownToHtml(ARTIFACT_FENCE));
const text = editor.getText();
editor.destroy();

expect(text).toContain('Dict\nfrom functools');
expect(text).toContain('@log_execution\ndef process_data_batch');
expect(text).toContain('"""\n @wraps(func)');
expect(text).not.toContain('Dictfrom');
expect(text).not.toContain('log_executiondef');
});

it('preserves the full fence body line count end to end', () => {
const editor = createEditor(markdownToHtml(ARTIFACT_FENCE));
const text = editor.getText();
editor.destroy();

const fenceBody = ARTIFACT_FENCE.split('\n').slice(1, -1);
for (const line of fenceBody) {
if (line.trim()) {
expect(text).toContain(line);
}
}
expect(text.split('\n').length).toBeGreaterThanOrEqual(fenceBody.length);
});
});
17 changes: 17 additions & 0 deletions components/canvas/__tests__/canvas-component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,23 @@ describe('CanvasComponent', () => {
expect(screen.getByText('Untitled Artifact')).toBeInTheDocument();
});

it('renders dark code blocks with a transparent inner code reset (issue #2109)', () => {
render(<CanvasComponent {...defaultProps} />);
const styles = Array.from(document.querySelectorAll('style'))
.map((s) => s.textContent || '')
.join('\n');
expect(styles).toContain('.ProseMirror pre code');
expect(styles).toMatch(
/\.ProseMirror pre code[\s\S]*?background: transparent !important/,
);
expect(styles).toMatch(
/\.ProseMirror pre code[\s\S]*?color: inherit !important/,
);
expect(styles).toMatch(
/\.ProseMirror pre,[\s\S]*?background: #1e1e1e !important/,
);
});

it('renders the FileText icon in header', () => {
render(<CanvasComponent {...defaultProps} />);
const svg = document.querySelector('svg.lucide-file-text');
Expand Down
18 changes: 18 additions & 0 deletions components/canvas/__tests__/canvas-rich-text-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,24 @@ describe('CanvasRichTextEditor', () => {
expect(result.current).toBeDefined();
});

it('scopes inline-code chip styling away from code blocks (issue #2109)', async () => {
const { useEditor } = await import('@tiptap/react');
(useEditor as any).mockClear();
renderHook(() =>
useCanvasRichTextEditor({
value: 'Test content',
onChange: vi.fn(),
}),
);
const options = (useEditor as any).mock.calls[0][0];
const editorClass = options.editorProps.attributes.class as string;
expect(editorClass).toContain('[&_:not(pre)>code]:bg-muted');
expect(editorClass).toContain('[&_pre_code]:bg-transparent');
expect(editorClass).toContain('[&_pre_code]:p-0');
expect(editorClass).not.toContain('[&_code]:bg-muted');
expect(editorClass).not.toContain('[&_pre]:bg-muted');
});

it('handles markdown value', () => {
const { result } = renderHook(() =>
useCanvasRichTextEditor({
Expand Down
10 changes: 8 additions & 2 deletions components/canvas/canvas-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ import { useCanvasVersionNavigation } from '@/hooks/use-canvas-version-navigatio

// Import KaTeX CSS for math rendering
import 'katex/dist/katex.min.css';
// Import highlight.js theme for code syntax highlighting
import 'highlight.js/styles/github.css';

interface CanvasComponentProps {
title?: string;
Expand Down Expand Up @@ -2497,6 +2495,14 @@ export function CanvasComponent({
[contenteditable] code {
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace !important;
}
.ProseMirror pre code,
[contenteditable] pre code {
background: transparent !important;
color: inherit !important;
padding: 0 !important;
border-radius: 0 !important;
font-size: inherit !important;
}
.ProseMirror :not(pre) > code,
[contenteditable] :not(pre) > code {
background: #e5e7eb !important;
Expand Down
5 changes: 3 additions & 2 deletions components/canvas/canvas-rich-text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -963,8 +963,9 @@ export function useCanvasRichTextEditor({
'[&_ol_ol_ol]:list-[lower-roman] [&_ol_ol_ol]:pl-4 ' +
'[&_li]:leading-7 [&_li]:pl-1 ' +
// Code
'[&_code]:bg-muted [&_code]:rounded [&_code]:px-1 [&_code]:py-0.5 ' +
'[&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:p-4 [&_pre]:mb-4 ' +
'[&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:rounded [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 ' +
'[&_pre]:rounded-md [&_pre]:p-4 [&_pre]:mb-4 ' +
'[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit ' +
// Blockquote
'[&_blockquote]:border-l-4 [&_blockquote]:border-primary [&_blockquote]:pl-4 [&_blockquote]:mb-4 [&_blockquote]:italic [&_blockquote]:leading-7 ' +
// Links
Expand Down
3 changes: 2 additions & 1 deletion components/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ReactMarkdown from 'react-markdown';
import 'katex/dist/katex.min.css';
import { cn } from '@/lib/utils';
import { preprocessLaTeX } from '@/lib/preprocess-latex';
import { normalizeListIndentation } from '@/lib/normalize-list-indentation';
import { components } from './markdown/markdown-components';

type Props = {
Expand Down Expand Up @@ -34,7 +35,7 @@ export default function Markdown({ children, className }: Props) {
return '';
}}
>
{preprocessLaTeX(children ?? '')}
{normalizeListIndentation(preprocessLaTeX(children ?? ''))}
</ReactMarkdown>
</div>
);
Expand Down
24 changes: 24 additions & 0 deletions components/markdown/__tests__/markdown-components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ describe('Markdown Components', () => {
expect(ul).toBeTruthy();
expect(ul?.className).toContain('list-disc');
});

it('should tighten its margins when nested inside another list', () => {
const { container } = render(
<Ul node={{} as any}>
<li>Item</li>
</Ul>,
);
const ul = container.querySelector('ul');
expect(ul?.className).toContain('my-6');
expect(ul?.className).toContain('[ul_&]:my-1');
expect(ul?.className).toContain('[ol_&]:my-1');
});
});

describe('ol component', () => {
Expand All @@ -320,6 +332,18 @@ describe('Markdown Components', () => {
expect(ol).toBeTruthy();
expect(ol?.className).toContain('list-decimal');
});

it('should tighten its margins when nested inside another list', () => {
const { container } = render(
<Ol node={{} as any}>
<li>Item</li>
</Ol>,
);
const ol = container.querySelector('ol');
expect(ol?.className).toContain('my-6');
expect(ol?.className).toContain('[ul_&]:my-1');
expect(ol?.className).toContain('[ol_&]:my-1');
});
});

describe('li component', () => {
Expand Down
14 changes: 12 additions & 2 deletions components/markdown/markdown-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,22 @@ export const components: Components = {
/>
),

// A nested list keeps the top-level my-6 unless overridden, which detaches
// it from its parent item by 24px on each side. The [ul_&]/[ol_&] variants
// win on specificity (.x ul beats .x) and collapse the gap only when the
// list sits inside another list.
ul: ({ node, ...props }) => (
<ul {...props} className="my-6 ml-6 list-disc [&>li]:mt-2" />
<ul
{...props}
className="my-6 ml-6 list-disc [&>li]:mt-2 [ol_&]:my-1 [ul_&]:my-1"
/>
),

ol: ({ node, ...props }) => (
<ol {...props} className="my-6 ml-6 list-decimal [&>li]:mt-2" />
<ol
{...props}
className="my-6 ml-6 list-decimal [&>li]:mt-2 [ol_&]:my-1 [ul_&]:my-1"
/>
),

// The scroll container has to be inside the <li>: a list item that is itself
Expand Down
13 changes: 6 additions & 7 deletions e2e/journeys/61-latex-math-rendering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,12 @@ test.describe('Journey 61: LaTeX / Math Rendering', () => {
chatPage,
}) => {
// `remark-math` only classifies a `$$...$$` span as BLOCK (display) math
// when the delimiters each sit alone on their own line — confirmed by
// probing this exact build: a single-line `$$3x + 5$$` renders as
// inline `.katex` inside a `<p>` (no `.katex-display`), while the
// "own-line fence" form below renders `<span class="katex-display">`
// with `display="block"` in the MathML. This is also the form LLMs
// naturally emit for standalone equations, so it's the realistic case
// for the "block math" checkpoint.
// when the delimiters each sit alone on their own line. `preprocessLaTeX`
// now expands a whole-line single-line `$$3x + 5$$` into this fenced form
// (issue #2109 fix 9), so the fence below is both what LLMs naturally
// emit for standalone equations and the canonical shape every whole-line
// `$$...$$` is normalized to before rendering — the realistic case for
// the "block math" checkpoint.
const content = [
'$$',
'3x + 5',
Expand Down
11 changes: 10 additions & 1 deletion lib/__tests__/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,16 @@ describe('mentor constants', () => {
it('should have all default prompts', () => {
expect(DEFAULT_PROMPTS.DEFAULT_SYSTEM_PROMPT).toBeTruthy();
expect(DEFAULT_PROMPTS.DEFAULT_SYSTEM_PROMPT).toContain(
'helpful instructor',
'helpful assistant',
);
// LaTeX is scoped to math delimiters only. The old prompt's "Always use
// LaTeX formatting for presenting your responses" made models emit
// \begin{itemize}/\textbf document markup that KaTeX cannot render
// (issue #2109) -- it must never come back.
expect(DEFAULT_PROMPTS.DEFAULT_SYSTEM_PROMPT).toContain('$...$');
expect(DEFAULT_PROMPTS.DEFAULT_SYSTEM_PROMPT).toContain('$$...$$');
expect(DEFAULT_PROMPTS.DEFAULT_SYSTEM_PROMPT).not.toMatch(
/LaTeX formatting for presenting/i,
);

expect(DEFAULT_PROMPTS.DEFAULT_MODERATION_PROMPT).toBeTruthy();
Expand Down
Loading
Loading