mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 03:16:25 +02:00
feat: enhance Streamdown plugins for code and math rendering in MarkdownViewer component
This commit is contained in:
parent
f96e7e11c6
commit
207b9e0ed3
4 changed files with 202 additions and 1063 deletions
|
|
@ -208,3 +208,5 @@ button {
|
||||||
|
|
||||||
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
|
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
|
||||||
@source '../node_modules/streamdown/dist/*.js';
|
@source '../node_modules/streamdown/dist/*.js';
|
||||||
|
@source '../node_modules/@streamdown/code/dist/*.js';
|
||||||
|
@source '../node_modules/@streamdown/math/dist/*.js';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,68 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Streamdown, type StreamdownProps } from "streamdown";
|
import { Streamdown, type StreamdownProps } from "streamdown";
|
||||||
|
import { createCodePlugin } from "@streamdown/code";
|
||||||
|
import { createMathPlugin } from "@streamdown/math";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const code = createCodePlugin({
|
||||||
|
themes: ["nord", "nord"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const math = createMathPlugin({
|
||||||
|
singleDollarTextMath: true,
|
||||||
|
});
|
||||||
|
|
||||||
interface MarkdownViewerProps {
|
interface MarkdownViewerProps {
|
||||||
content: string;
|
content: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the entire content is wrapped in a single ```markdown or ```md
|
||||||
|
* code fence, strip the fence so the inner markdown renders properly.
|
||||||
|
*/
|
||||||
|
function stripOuterMarkdownFence(content: string): string {
|
||||||
|
const trimmed = content.trim();
|
||||||
|
const match = trimmed.match(
|
||||||
|
/^```(?:markdown|md)?\s*\n([\s\S]+?)\n```\s*$/
|
||||||
|
);
|
||||||
|
return match ? match[1] : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert various LaTeX delimiter styles to the dollar-sign syntax
|
||||||
|
* that remark-math understands, and normalise edge-cases that
|
||||||
|
* commonly appear in LLM-generated markdown.
|
||||||
|
*
|
||||||
|
* \[...\] → $$ ... $$ (block / display math)
|
||||||
|
* \(...\) → $ ... $ (inline math)
|
||||||
|
* same-line $$…$$ → $ ... $ (inline math — display math
|
||||||
|
* can't live inside table cells)
|
||||||
|
* `$$ … $$` → $$ … $$ (strip wrapping backtick code)
|
||||||
|
* `$ … $` → $ … $ (strip wrapping backtick code)
|
||||||
|
*/
|
||||||
|
function convertLatexDelimiters(content: string): string {
|
||||||
|
// 1. Block math: \[...\] → $$...$$
|
||||||
|
content = content.replace(/\\\[([\s\S]*?)\\\]/g, (_match, inner) => {
|
||||||
|
return `$$${inner}$$`;
|
||||||
|
});
|
||||||
|
// 2. Inline math: \(...\) → $...$
|
||||||
|
content = content.replace(/\\\(([\s\S]*?)\\\)/g, (_match, inner) => {
|
||||||
|
return `$${inner}$`;
|
||||||
|
});
|
||||||
|
// 3. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$
|
||||||
|
content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1");
|
||||||
|
// 4. Same-line $$...$$ → $...$ (inline math) so it works inside table cells.
|
||||||
|
// True display math has $$ on its own line, so this only affects inline usage.
|
||||||
|
content = content.replace(/\$\$([^\n]+?)\$\$/g, (_match, inner) => {
|
||||||
|
return `$${inner}$`;
|
||||||
|
});
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
|
export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
|
||||||
|
const processedContent = convertLatexDelimiters(stripOuterMarkdownFence(content));
|
||||||
const components: StreamdownProps["components"] = {
|
const components: StreamdownProps["components"] = {
|
||||||
p: ({ children, ...props }) => (
|
p: ({ children, ...props }) => (
|
||||||
<p className="my-2" {...props}>
|
<p className="my-2" {...props}>
|
||||||
|
|
@ -62,28 +117,31 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
table: ({ ...props }) => (
|
table: ({ ...props }) => (
|
||||||
<div className="overflow-x-auto my-4">
|
<div className="overflow-x-auto my-4 rounded-lg border border-border w-full">
|
||||||
<table className="min-w-full divide-y divide-border" {...props} />
|
<table className="w-full divide-y divide-border" {...props} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
th: ({ ...props }) => <th className="px-3 py-2 text-left font-medium bg-muted" {...props} />,
|
th: ({ ...props }) => <th className="px-4 py-2.5 text-left text-sm font-semibold text-muted-foreground/80 bg-muted/30 border-r border-border/40 last:border-r-0" {...props} />,
|
||||||
td: ({ ...props }) => <td className="px-3 py-2 border-t border-border" {...props} />,
|
td: ({ ...props }) => <td className="px-4 py-2.5 text-sm border-t border-r border-border/40 last:border-r-0" {...props} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"prose prose-sm dark:prose-invert max-w-none overflow-hidden [&_table]:block [&_table]:overflow-x-auto",
|
"max-w-none overflow-hidden",
|
||||||
|
"[&_[data-streamdown=code-block-header]]:!bg-transparent",
|
||||||
|
"[&_[data-streamdown=code-block]>*]:!border-none [&_[data-streamdown=code-block]>*]:![box-shadow:none]",
|
||||||
|
"[&_[data-streamdown=code-block-download-button]]:!hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
components={components}
|
components={components}
|
||||||
shikiTheme={["github-light", "github-dark"]}
|
plugins={{ code, math }}
|
||||||
controls={{ code: true }}
|
controls={{ code: true }}
|
||||||
mode="static"
|
mode="static"
|
||||||
>
|
>
|
||||||
{content}
|
{processedContent}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@streamdown/code": "^1.0.2",
|
||||||
|
"@streamdown/math": "^1.0.2",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.1",
|
||||||
"@tanstack/query-core": "^5.90.7",
|
"@tanstack/query-core": "^5.90.7",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.7",
|
||||||
|
|
@ -79,6 +81,7 @@
|
||||||
"geist": "^1.4.2",
|
"geist": "^1.4.2",
|
||||||
"jotai": "^2.15.1",
|
"jotai": "^2.15.1",
|
||||||
"jotai-tanstack-query": "^0.11.0",
|
"jotai-tanstack-query": "^0.11.0",
|
||||||
|
"katex": "^0.16.28",
|
||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.477.0",
|
||||||
"motion": "^12.23.22",
|
"motion": "^12.23.22",
|
||||||
"next": "^16.1.0",
|
"next": "^16.1.0",
|
||||||
|
|
@ -101,7 +104,7 @@
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"streamdown": "^1.6.10",
|
"streamdown": "^2.2.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
|
|
|
||||||
1186
surfsense_web/pnpm-lock.yaml
generated
1186
surfsense_web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue