Update Katex styles and rendering in markdown content

This commit is contained in:
zaidmukaddam 2024-09-18 20:50:51 +05:30
parent 6ec4100e50
commit 00f9923ed7
6 changed files with 112 additions and 50 deletions

View File

@ -19,6 +19,33 @@ body {
inset 0 1px 0 0 #ffffff52; inset 0 1px 0 0 #ffffff52;
} }
.markdown-body .katex {
font-size: 1.1em;
}
.markdown-body .katex-display {
overflow-x: auto;
overflow-y: hidden;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-top: 1em;
margin-bottom: 1em;
}
.markdown-body .katex-display > .katex {
font-size: 1.21em;
}
.markdown-body .katex-display > .katex > .katex-html {
display: block;
position: relative;
}
.markdown-body .katex-display > .katex > .katex-html > .tag {
position: absolute;
right: 0;
}
@layer utilities { @layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */

View File

@ -1,4 +1,5 @@
import "./globals.css"; import "./globals.css";
import 'katex/dist/katex.min.css';
import { Metadata, Viewport } from "next"; import { Metadata, Viewport } from "next";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { Inter, Instrument_Serif, IBM_Plex_Mono } from 'next/font/google'; import { Inter, Instrument_Serif, IBM_Plex_Mono } from 'next/font/google';

View File

@ -1,5 +1,6 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
"use client"; "use client";
import 'katex/dist/katex.min.css';
import import
React, React,
@ -15,9 +16,9 @@ React,
import ReactMarkdown, { Components } from 'react-markdown'; import ReactMarkdown, { Components } from 'react-markdown';
import Marked, { ReactRenderer } from 'marked-react'; import Marked, { ReactRenderer } from 'marked-react';
import katex from 'katex'; import katex from 'katex';
import Latex from 'react-latex-next';
import { track } from '@vercel/analytics'; import { track } from '@vercel/analytics';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import 'katex/dist/katex.min.css';
import { useChat } from 'ai/react'; import { useChat } from 'ai/react';
import { ToolInvocation } from 'ai'; import { ToolInvocation } from 'ai';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -1624,21 +1625,12 @@ The o1-mini is a new OpenAI model that is optimized for reasoning tasks. Current
return metadata; return metadata;
}, [metadataCache]); }, [metadataCache]);
const renderEquation = (equation: string): string => { const preprocessContent = (text: string) => {
try { // Replace block-level LaTeX
return katex.renderToString(equation, { text = text.replace(/\$\$(.*?)\$\$/g, '\\[$1\\]');
throwOnError: false, // Replace bracket-enclosed LaTeX
displayMode: true, text = text.replace(/\[(.*?)\]/g, '\\[$1\\]');
strict: false, return text;
trust: true,
macros: {
"\\,": "\\:"
}
});
} catch (error) {
console.error("KaTeX rendering error:", error);
return equation;
}
}; };
const CodeBlock = ({ language, children }: { language: string | undefined; children: string }) => { const CodeBlock = ({ language, children }: { language: string | undefined; children: string }) => {
@ -1721,10 +1713,6 @@ The o1-mini is a new OpenAI model that is optimized for reasoning tasks. Current
</p> </p>
)} )}
</div> </div>
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 text-xs text-gray-500">
<span className="truncate max-w-[80%]">{href}</span>
<ExternalLink size={14} className="flex-shrink-0" />
</div>
</div> </div>
); );
}; };
@ -1753,27 +1741,50 @@ The o1-mini is a new OpenAI model that is optimized for reasoning tasks. Current
); );
}; };
const latexMacros = {
"\\display": "\\displaystyle",
};
const renderer: Partial<ReactRenderer> = { const renderer: Partial<ReactRenderer> = {
paragraph(children) { paragraph(children) {
if (typeof children === 'string') { return (
const parts = children.split(/(\[.*?\])/g); <p className="my-4">
return ( {React.Children.map(children, (child) => {
<p> if (typeof child === 'string') {
{parts.map((part, index) => { // Split the string to handle inline and display equations separately
if (part.startsWith('[') && part.endsWith(']')) { const parts = child.split(/(\\\[.*?\\\]|\$.*?\$)/gs);
const equation = part.slice(1, -1); return parts.map((part, index) => {
return ( if (part.startsWith('\\[') && part.endsWith('\\]')) {
<span key={index} dangerouslySetInnerHTML={{ // Display mode equation
__html: renderEquation(equation) return (
}} /> <Latex key={index} macros={latexMacros}>
); {part}
} </Latex>
return <span key={index}>{part}</span>; );
})} } else if (part.startsWith('$') && part.endsWith('$')) {
</p> // Inline equation
); return (
} <Latex key={index} macros={latexMacros}>
return <p>{children}</p>; {part}
</Latex>
);
} // add $$ for display mode equations
else if (part.startsWith('$$') && part.endsWith('$$')) {
// Display mode equation
return (
<Latex key={index} macros={latexMacros}>
{part}
</Latex>
);
}
// Regular text
return part;
});
}
return child;
})}
</p>
);
}, },
code(children, language) { code(children, language) {
return <CodeBlock language={language}>{String(children)}</CodeBlock>; return <CodeBlock language={language}>{String(children)}</CodeBlock>;
@ -1787,7 +1798,7 @@ The o1-mini is a new OpenAI model that is optimized for reasoning tasks. Current
</sup> </sup>
); );
} }
return isValidUrl(href) ? renderHoverCard(href, text) : <a href={href}>{text}</a>; return isValidUrl(href) ? renderHoverCard(href, text) : <a href={href} className="text-blue-600 hover:underline">{text}</a>;
}, },
heading(children, level) { heading(children, level) {
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
@ -1796,19 +1807,21 @@ The o1-mini is a new OpenAI model that is optimized for reasoning tasks. Current
}, },
list(children, ordered) { list(children, ordered) {
const ListTag = ordered ? 'ol' : 'ul'; const ListTag = ordered ? 'ol' : 'ul';
return <ListTag className="list-inside list-disc my-2">{children}</ListTag>; return <ListTag className="list-inside list-disc my-4 pl-4">{children}</ListTag>;
}, },
listItem(children) { listItem(children) {
return <li className="my-1">{children}</li>; return <li className="my-2">{children}</li>;
}, },
blockquote(children) { blockquote(children) {
return <blockquote className="border-l-4 border-gray-300 pl-4 italic my-4">{children}</blockquote>; return <blockquote className="border-l-4 border-gray-300 pl-4 italic my-4">{children}</blockquote>;
}, },
}; };
const preprocessedContent = useMemo(() => preprocessContent(content), [content]);
return ( return (
<div className="prose prose-sm sm:prose-base max-w-none"> <div className="markdown-body">
<Marked renderer={renderer}>{content}</Marked> <Marked renderer={renderer}>{preprocessedContent}</Marked>
</div> </div>
); );
}; };

View File

@ -53,6 +53,7 @@
"openai": "^4.56.0", "openai": "^4.56.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-latex-next": "^3.0.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tweet": "^3.2.1", "react-tweet": "^3.2.1",

View File

@ -85,7 +85,7 @@ dependencies:
version: 1.4.0 version: 1.4.0
ai: ai:
specifier: latest specifier: latest
version: 3.3.40(openai@4.56.0)(react@18.3.1)(svelte@4.2.18)(vue@3.4.35)(zod@3.23.8) version: 3.3.41(openai@4.56.0)(react@18.3.1)(svelte@4.2.18)(vue@3.4.35)(zod@3.23.8)
anthropic-vertex-ai: anthropic-vertex-ai:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0(zod@3.23.8) version: 1.0.0(zod@3.23.8)
@ -137,6 +137,9 @@ dependencies:
react-dom: react-dom:
specifier: ^18 specifier: ^18
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
react-latex-next:
specifier: ^3.0.0
version: 3.0.0(react-dom@18.3.1)(react@18.3.1)
react-markdown: react-markdown:
specifier: ^9.0.1 specifier: ^9.0.1
version: 9.0.1(@types/react@18.3.3)(react@18.3.1) version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
@ -424,8 +427,8 @@ packages:
zod-to-json-schema: 3.23.2(zod@3.23.8) zod-to-json-schema: 3.23.2(zod@3.23.8)
dev: false dev: false
/@ai-sdk/vue@0.0.49(vue@3.4.35)(zod@3.23.8): /@ai-sdk/vue@0.0.50(vue@3.4.35)(zod@3.23.8):
resolution: {integrity: sha512-GLjk5uhn0dA8iXpqdF91NyOw+VCgDIo22zrdkRtDg+nLaqkFSjgdDLAp7CL+ihW4F0/IkpZym3j0lFi9LiCjZA==} resolution: {integrity: sha512-eIWfxqpKwRdL3rxJMg1HDJcjfugFJGg4P934Tl69S7UCot2/U4BPZoESVJQFroS1elbKHaMRgv0ZJt1ddWQPjQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
vue: ^3.3.4 vue: ^3.3.4
@ -1921,8 +1924,8 @@ packages:
humanize-ms: 1.2.1 humanize-ms: 1.2.1
dev: false dev: false
/ai@3.3.40(openai@4.56.0)(react@18.3.1)(svelte@4.2.18)(vue@3.4.35)(zod@3.23.8): /ai@3.3.41(openai@4.56.0)(react@18.3.1)(svelte@4.2.18)(vue@3.4.35)(zod@3.23.8):
resolution: {integrity: sha512-EqCrKHKO0TIsZANJEO3QY9uNXiZC9owhpE/jn9Yt83ET3qPrdIi3zbx/MXX8F6ivmKt2vzoYzR4a5Z7l9kJLJQ==} resolution: {integrity: sha512-unWUqw0hnZo0irhdedTv8ef7IEiySBCO3zjPxx1/k0kI1G0whKYq8l83k/LzqShLekc2Qg3gyyhdEO+39ptegw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
openai: ^4.42.0 openai: ^4.42.0
@ -1948,7 +1951,7 @@ packages:
'@ai-sdk/solid': 0.0.47(zod@3.23.8) '@ai-sdk/solid': 0.0.47(zod@3.23.8)
'@ai-sdk/svelte': 0.0.49(svelte@4.2.18)(zod@3.23.8) '@ai-sdk/svelte': 0.0.49(svelte@4.2.18)(zod@3.23.8)
'@ai-sdk/ui-utils': 0.0.44(zod@3.23.8) '@ai-sdk/ui-utils': 0.0.44(zod@3.23.8)
'@ai-sdk/vue': 0.0.49(vue@3.4.35)(zod@3.23.8) '@ai-sdk/vue': 0.0.50(vue@3.4.35)(zod@3.23.8)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
eventsource-parser: 1.1.2 eventsource-parser: 1.1.2
json-schema: 0.4.0 json-schema: 0.4.0
@ -5347,6 +5350,18 @@ packages:
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
/react-latex-next@3.0.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==}
engines: {node: '>=12', npm: '>=5'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
katex: 0.16.11
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1): /react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
peerDependencies: peerDependencies:

View File

@ -84,6 +84,11 @@ const config = {
}, },
}, },
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
safelist: [
{
pattern: /katex-.*/,
},
],
} satisfies Config } satisfies Config
export default config export default config