feat: implement markdown codeblock

This commit is contained in:
steven
2023-03-27 11:36:45 +08:00
parent 0c8e2587a4
commit 9c9204494d
5 changed files with 862 additions and 22 deletions

View File

@ -1,7 +1,9 @@
import { marked } from "marked"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
import { Message } from "@/types"; import { Message } from "@/types";
import Icon from "../Icon"; import Icon from "../Icon";
import { CodeBlock } from "../CodeBlock";
interface Props { interface Props {
message: Message; message: Message;
@ -28,10 +30,32 @@ const MessageView = (props: Props) => {
<div className="w-10 h-10 p-1 border rounded-full flex justify-center items-center mr-2 shrink-0"> <div className="w-10 h-10 p-1 border rounded-full flex justify-center items-center mr-2 shrink-0">
<Icon.AiOutlineRobot className="w-6 h-6" /> <Icon.AiOutlineRobot className="w-6 h-6" />
</div> </div>
<div <ReactMarkdown
className="mt-0.5 w-auto max-w-full bg-gray-100 px-4 py-2 rounded-lg rounded-tl-none shadow prose prose-neutral" className="mt-0.5 w-auto max-w-full bg-gray-100 px-4 py-2 rounded-lg rounded-tl-none shadow prose prose-neutral"
dangerouslySetInnerHTML={{ __html: marked.parse(message.content) }} remarkPlugins={[remarkGfm]}
></div> components={{
pre({ node, className, children, ...props }) {
return (
<pre className={`${className || ""} p-0 w-full`} {...props}>
{children}
</pre>
);
},
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
const language = match ? match[1] : "plain";
return !inline ? (
<CodeBlock key={Math.random()} language={language || "plain"} value={String(children).replace(/\n$/, "")} {...props} />
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{message.content}
</ReactMarkdown>
</> </>
)} )}
</div> </div>

View File

@ -10,10 +10,11 @@ const ClearDataConfirmModal = (props: Props) => {
const handleClearData = () => { const handleClearData = () => {
window.localStorage.clear(); window.localStorage.clear();
close();
toast.success("Message cleared. The page will be reloaded."); toast.success("Message cleared. The page will be reloaded.");
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 300); }, 500);
}; };
return ( return (

45
components/CodeBlock.tsx Normal file
View File

@ -0,0 +1,45 @@
import { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
interface Props {
language: string;
value: string;
}
export const CodeBlock = (props: Props) => {
const { language, value } = props;
const [isCopied, setIsCopied] = useState<Boolean>(false);
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
};
return (
<div className="w-full relative font-sans text-[16px]">
<div className="flex items-center justify-between py-1.5 px-4">
<span className="text-xs text-white font-mono">{language}</span>
<div className="flex items-center">
<button
className="flex items-center rounded bg-none py-0.5 px-2 text-xs text-white bg-gray-600 hover:opacity-80"
onClick={copyToClipboard}
>
{isCopied ? "Copied!" : "Copy"}
</button>
</div>
</div>
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
{value}
</SyntaxHighlighter>
</div>
);
};

View File

@ -15,24 +15,25 @@
"eventsource-parser": "^1.0.0", "eventsource-parser": "^1.0.0",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^4.2.12",
"next": "^13.2.4", "next": "^13.2.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-markdown": "^8.0.6",
"react-textarea-autosize": "^8.4.0", "react-textarea-autosize": "^8.4.0",
"remark-gfm": "^3.0.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"zustand": "^4.3.6" "zustand": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",
"@types/marked": "^4.0.8",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "8.20.0", "eslint": "8.20.0",
@ -40,6 +41,7 @@
"mysql2": "^3.2.0", "mysql2": "^3.2.0",
"pg": "^8.10.0", "pg": "^8.10.0",
"postcss": "^8.4.20", "postcss": "^8.4.20",
"react-syntax-highlighter": "^15.5.0",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"typescript": "^4.9.4" "typescript": "^4.9.4"
} }

798
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff