mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-14 11:39:33 +00:00
feat: add remark-gfm to render table. improved display of inline code.
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user