feat: add remark-gfm to render table. improved display of inline code.

This commit is contained in:
Sayo
2025-10-31 07:42:55 +08:00
parent eda89ef147
commit eb835d21b2
3 changed files with 407 additions and 50 deletions

View File

@@ -18,6 +18,7 @@
import React, { useState, useEffect, useRef, useMemo, useCallback, memo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useDropzone } from 'react-dropzone';
import TodoList from './TodoList';
import ClaudeLogo from './ClaudeLogo.jsx';
@@ -44,6 +45,30 @@ function decodeHtmlEntities(text) {
.replace(/&/g, '&');
}
// Normalize markdown text where providers mistakenly wrap short inline code with single-line triple fences.
// Only convert fences that do NOT contain any newline to avoid touching real code blocks.
function normalizeInlineCodeFences(text) {
if (!text || typeof text !== 'string') return text;
try {
// ```code``` -> `code`
return text.replace(/```\s*([^\n\r]+?)\s*```/g, '`$1`');
} catch {
return text;
}
}
// Small wrapper to keep markdown behavior consistent in one place
const Markdown = ({ children, className }) => {
const content = normalizeInlineCodeFences(String(children ?? ''));
return (
<div className={className}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{content}
</ReactMarkdown>
</div>
);
};
// Format "Claude AI usage limit reached|<epoch>" into a local time string
function formatUsageLimitText(text) {
try {
@@ -169,6 +194,63 @@ const safeLocalStorage = {
}
};
// Common markdown components to ensure consistent rendering (tables, inline code, links, etc.)
const markdownComponents = {
code: ({ node, inline, className, children, ...props }) => {
const raw = Array.isArray(children) ? children.join('') : String(children ?? '');
const looksMultiline = /[\r\n]/.test(raw);
const inlineDetected = inline || (node && node.type === 'inlineCode');
const shouldInline = inlineDetected || !looksMultiline; // fallback to inline if single-line
if (shouldInline) {
return (
<code
className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800/60 dark:text-gray-100 dark:border-gray-700 whitespace-pre-wrap break-words ${
className || ''
}`}
{...props}
>
{children}
</code>
);
}
return (
<pre className="bg-gray-900 dark:bg-gray-900 border border-gray-700/40 rounded-lg p-3 overflow-x-auto my-2">
<code className={`text-gray-100 dark:text-gray-100 text-sm font-mono ${className || ''}`} {...props}>
{children}
</code>
</pre>
);
},
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-2">
{children}
</blockquote>
),
a: ({ href, children }) => (
<a href={href} className="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener noreferrer">
{children}
</a>
),
p: ({ children }) => <div className="mb-2 last:mb-0">{children}</div>,
// GFM tables
table: ({ children }) => (
<div className="overflow-x-auto my-2">
<table className="min-w-full border-collapse border border-gray-200 dark:border-gray-700">
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>
),
th: ({ children }) => (
<th className="px-3 py-2 text-left text-sm font-semibold border border-gray-200 dark:border-gray-700">{children}</th>
),
td: ({ children }) => (
<td className="px-3 py-2 align-top text-sm border border-gray-200 dark:border-gray-700">{children}</td>
)
};
// Memoized message component to prevent unnecessary re-renders
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters, showThinking }) => {
const isGrouped = prevMessage && prevMessage.type === message.type &&
@@ -607,9 +689,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
</svg>
📋 View implementation plan
</summary>
<div className="mt-3 prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{planContent}</ReactMarkdown>
</div>
<Markdown className="mt-3 prose prose-sm max-w-none dark:prose-invert">
{planContent}
</Markdown>
</details>
);
}
@@ -717,9 +799,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
<div className="flex items-center gap-2 mb-3">
<span className="font-medium">Implementation Plan</span>
</div>
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{planContent}</ReactMarkdown>
</div>
<Markdown className="prose prose-sm max-w-none dark:prose-invert">
{planContent}
</Markdown>
</div>
);
}
@@ -909,17 +991,17 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
</svg>
View full output ({content.length} chars)
</summary>
<div className="mt-2 prose prose-sm max-w-none prose-green dark:prose-invert">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
<Markdown className="mt-2 prose prose-sm max-w-none prose-green dark:prose-invert">
{content}
</Markdown>
</details>
);
}
return (
<div className="prose prose-sm max-w-none prose-green dark:prose-invert">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
<Markdown className="prose prose-sm max-w-none prose-green dark:prose-invert">
{content}
</Markdown>
);
})()}
</div>
@@ -1115,42 +1197,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
// Normal rendering for non-JSON content
return message.type === 'assistant' ? (
<div className="prose prose-sm max-w-none dark:prose-invert prose-gray [&_code]:!bg-transparent [&_code]:!p-0 [&_pre]:!bg-transparent [&_pre]:!border-0 [&_pre]:!p-0">
<ReactMarkdown
components={{
code: ({node, inline, className, children, ...props}) => {
return inline ? (
<strong className="text-blue-600 dark:text-blue-400 font-bold not-prose" {...props}>
{children}
</strong>
) : (
<div className="bg-gray-800 dark:bg-gray-800 border border-gray-600/30 dark:border-gray-600/30 p-3 rounded-lg overflow-hidden my-2">
<code className="text-gray-100 dark:text-gray-200 text-sm font-mono block whitespace-pre-wrap break-words" {...props}>
{children}
</code>
</div>
);
},
blockquote: ({children}) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-2">
{children}
</blockquote>
),
a: ({href, children}) => (
<a href={href} className="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener noreferrer">
{children}
</a>
),
p: ({children}) => (
<div className="mb-2 last:mb-0">
{children}
</div>
)
}}
>
{content}
</ReactMarkdown>
</div>
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray">
{content}
</Markdown>
) : (
<div className="whitespace-pre-wrap">
{content}
@@ -4347,4 +4396,4 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
);
}
export default React.memo(ChatInterface);
export default React.memo(ChatInterface);