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:
parent
6e3e67fc24
commit
924d459420
3 changed files with 132 additions and 19 deletions
|
|
@ -18,6 +18,16 @@ import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/pris
|
||||||
import { LoaderIcon, ChevronDownIcon } from 'lucide-react'
|
import { LoaderIcon, ChevronDownIcon } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 & {
|
export type MessageWithError = Message & {
|
||||||
id: string // Unique identifier for stable React keys
|
id: string // Unique identifier for stable React keys
|
||||||
isError?: boolean
|
isError?: boolean
|
||||||
|
|
@ -38,7 +48,7 @@ export type MessageWithError = Message & {
|
||||||
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop
|
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [katexPlugin, setKatexPlugin] = useState<any>(null)
|
const [katexPlugin, setKatexPlugin] = useState<((options?: KaTeXOptions) => any) | null>(null)
|
||||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState<boolean>(false)
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState<boolean>(false)
|
||||||
|
|
||||||
// Directly use props passed from the parent.
|
// Directly use props passed from the parent.
|
||||||
|
|
@ -64,26 +74,55 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKaTeX = async () => {
|
const loadKaTeX = async () => {
|
||||||
try {
|
try {
|
||||||
const [{ default: rehypeKatex }] = await Promise.all([
|
const { default: rehypeKatex } = await import('rehype-katex');
|
||||||
import('rehype-katex'),
|
setKatexPlugin(() => rehypeKatex);
|
||||||
import('katex/dist/katex.min.css')
|
|
||||||
])
|
|
||||||
setKatexPlugin(() => rehypeKatex)
|
|
||||||
} catch (error) {
|
} 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(() => ({
|
const mainMarkdownComponents = useMemo(() => ({
|
||||||
code: (props: any) => (
|
code: (props: any) => {
|
||||||
<CodeHighlight
|
const { inline, className, children, ...restProps } = props;
|
||||||
{...props}
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
renderAsDiagram={message.mermaidRendered ?? false}
|
const language = match ? match[1] : undefined;
|
||||||
messageRole={message.role}
|
|
||||||
/>
|
// 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
|
||||||
|
inline={inline}
|
||||||
|
className={className}
|
||||||
|
{...restProps}
|
||||||
|
renderAsDiagram={message.mermaidRendered ?? false}
|
||||||
|
messageRole={message.role}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CodeHighlight>
|
||||||
|
);
|
||||||
|
},
|
||||||
p: ({ children }: { children?: ReactNode }) => <div className="my-2">{children}</div>,
|
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>,
|
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>,
|
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,
|
throwOnError: false,
|
||||||
displayMode: false,
|
displayMode: false,
|
||||||
strict: 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] : []),
|
}] as any] : []),
|
||||||
rehypeReact
|
rehypeReact
|
||||||
]}
|
]}
|
||||||
|
|
@ -182,7 +228,14 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
displayMode: false,
|
displayMode: false,
|
||||||
strict: 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] : []),
|
] as any] : []),
|
||||||
rehypeReact
|
rehypeReact
|
||||||
|
|
|
||||||
|
|
@ -167,3 +167,62 @@
|
||||||
background-color: hsl(0 0% 0%);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import AppRouter from './AppRouter'
|
import AppRouter from './AppRouter'
|
||||||
import './i18n.ts';
|
import './i18n.ts';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue