feat(webui): Enhance KaTeX rendering and add robust error handling

- Differentiates between inline ($...$) and display ($$..$$) math for proper styling and layout.
- Adds custom CSS to ensure formulas correctly inherit text color, fixing issues in dark/light themes.
- Implements responsive handling for long formulas by allowing horizontal scrolling, preventing page overflow.
- Introduces a silent `errorCallback` for KaTeX to suppress console errors from invalid LaTeX syntax in production, while retaining warnings in development.
- Refactors KaTeX plugin loading to be more robust and simplifies CSS import by moving it to `main.tsx`.
This commit is contained in:
yangdx 2025-09-28 22:50:33 +08:00
parent 6e3e67fc24
commit 924d459420
3 changed files with 132 additions and 19 deletions

View file

@ -18,6 +18,16 @@ import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/pris
import { LoaderIcon, ChevronDownIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
// KaTeX configuration options interface
interface KaTeXOptions {
errorColor?: string;
throwOnError?: boolean;
displayMode?: boolean;
strict?: boolean;
trust?: boolean;
errorCallback?: (error: string, latex: string) => void;
}
export type MessageWithError = Message & {
id: string // Unique identifier for stable React keys
isError?: boolean
@ -38,7 +48,7 @@ export type MessageWithError = Message & {
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop
const { t } = useTranslation()
const { theme } = useTheme()
const [katexPlugin, setKatexPlugin] = useState<any>(null)
const [katexPlugin, setKatexPlugin] = useState<((options?: KaTeXOptions) => any) | null>(null)
const [isThinkingExpanded, setIsThinkingExpanded] = useState<boolean>(false)
// Directly use props passed from the parent.
@ -64,26 +74,55 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
useEffect(() => {
const loadKaTeX = async () => {
try {
const [{ default: rehypeKatex }] = await Promise.all([
import('rehype-katex'),
import('katex/dist/katex.min.css')
])
setKatexPlugin(() => rehypeKatex)
const { default: rehypeKatex } = await import('rehype-katex');
setKatexPlugin(() => rehypeKatex);
} catch (error) {
console.error('Failed to load KaTeX:', error)
console.error('Failed to load KaTeX plugin:', error);
// Set to null to ensure we don't try to use a failed plugin
setKatexPlugin(null);
}
}
loadKaTeX()
}, [])
};
loadKaTeX();
}, []);
const mainMarkdownComponents = useMemo(() => ({
code: (props: any) => (
code: (props: any) => {
const { inline, className, children, ...restProps } = props;
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : undefined;
// Handle math blocks ($$...$$) - provide better container and styling
if (language === 'math' && !inline) {
return (
<div className="katex-display-wrapper my-4 overflow-x-auto">
<div className="text-current">{children}</div>
</div>
);
}
// Handle inline math ($...$) - ensure proper inline display
if (language === 'math' && inline) {
return (
<span className="katex-inline-wrapper">
<span className="text-current">{children}</span>
</span>
);
}
// Handle all other code (inline and block)
return (
<CodeHighlight
{...props}
inline={inline}
className={className}
{...restProps}
renderAsDiagram={message.mermaidRendered ?? false}
messageRole={message.role}
/>
),
>
{children}
</CodeHighlight>
);
},
p: ({ children }: { children?: ReactNode }) => <div className="my-2">{children}</div>,
h1: ({ children }: { children?: ReactNode }) => <h1 className="text-xl font-bold mt-4 mb-2">{children}</h1>,
h2: ({ children }: { children?: ReactNode }) => <h2 className="text-lg font-bold mt-4 mb-2">{children}</h2>,
@ -148,7 +187,14 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
throwOnError: false,
displayMode: false,
strict: false,
trust: true
trust: true,
// Add silent error handling to avoid console noise
errorCallback: (error: string, latex: string) => {
// Only show detailed errors in development environment
if (process.env.NODE_ENV === 'development') {
console.warn('KaTeX rendering error in thinking content:', error, 'for LaTeX:', latex);
}
}
}] as any] : []),
rehypeReact
]}
@ -182,7 +228,14 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
throwOnError: false,
displayMode: false,
strict: false,
trust: true
trust: true,
// Add silent error handling to avoid console noise
errorCallback: (error: string, latex: string) => {
// Only show detailed errors in development environment
if (process.env.NODE_ENV === 'development') {
console.warn('KaTeX rendering error in main content:', error, 'for LaTeX:', latex);
}
}
}
] as any] : []),
rehypeReact

View file

@ -167,3 +167,62 @@
background-color: hsl(0 0% 0%);
}
}
/* KaTeX Math Formula Styles */
.katex-display-wrapper {
text-align: center;
position: relative;
}
.katex-display-wrapper .katex-display {
margin: 0.5em 0;
text-align: center;
}
.katex-inline-wrapper .katex {
font-size: inherit;
line-height: inherit;
}
/* Ensure KaTeX formulas inherit color properly */
.katex .base {
color: inherit;
}
/* Improve KaTeX display for different themes */
.katex .mord,
.katex .mop,
.katex .mbin,
.katex .mrel,
.katex .mpunct,
.katex .mopen,
.katex .mclose,
.katex .minner {
color: inherit;
}
/* Fix KaTeX display overflow issues */
.katex-display {
overflow-x: auto;
overflow-y: hidden;
max-width: 100%;
}
.katex-display > .katex {
white-space: nowrap;
}
/* Improve KaTeX error display */
.katex .katex-error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid rgba(255, 0, 0, 0.3);
border-radius: 4px;
padding: 2px 4px;
color: #dc2626;
}
.dark .katex .katex-error {
background-color: rgba(255, 0, 0, 0.2);
border-color: rgba(255, 0, 0, 0.4);
color: #ef4444;
}

View file

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import AppRouter from './AppRouter'
import './i18n.ts';
import 'katex/dist/katex.min.css';