Skip to content

Commit

Permalink
Markdown rendering (#1544)
Browse files Browse the repository at this point in the history
* Add Markdown rendering with syntax highlighting

* Use lazy loading for Markdown rendering

* Use tailwind typography for Markdown styling

* Remove unnecessary styling around code blocks

* Use same style for all code blocks

* render markdown in message

* remove debug code

* remove change in tailwind config

* move import to top level

* remove memo callback check

* extract plugin

* tweak style

* render markdown in modal

---------

Co-authored-by: mashdragon <122402293+mashdragon@users.noreply.github.com>
  • Loading branch information
notmd and mashdragon committed Feb 15, 2023
1 parent d86bcfb commit 07bc2f6
Show file tree
Hide file tree
Showing 9 changed files with 2,759 additions and 121 deletions.
2,639 changes: 2,579 additions & 60 deletions website/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions website/package.json
Expand Up @@ -37,6 +37,7 @@
"@next-auth/prisma-adapter": "^1.0.5",
"@next/bundle-analyzer": "^13.1.6",
"@next/font": "^13.1.0",
"@nikolovlazar/chakra-ui-prose": "^1.2.1",
"@prisma/client": "^4.7.1",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-table": "^8.7.6",
Expand Down Expand Up @@ -66,6 +67,9 @@
"react-feature-flags": "^1.0.0",
"react-hook-form": "^7.42.1",
"react-i18next": "^12.1.4",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.3",
"storybook-addon-next-router": "^4.0.2",
"swr": "^2.0.0",
Expand All @@ -90,6 +94,7 @@
"@types/accept-language-parser": "^1.5.3",
"@types/node": "^18.11.17",
"@types/react": "18.0.26",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"babel-loader": "^8.3.0",
"cross-env": "^7.0.3",
Expand Down
34 changes: 12 additions & 22 deletions website/src/components/Messages/MessageTableEntry.tsx
@@ -1,6 +1,5 @@
import {
Avatar,
AvatarProps,
Badge,
Box,
Flex,
Expand Down Expand Up @@ -34,7 +33,7 @@ import {
import NextLink from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { useCallback, useEffect, useMemo, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { LabelMessagePopup } from "src/components/Messages/LabelPopup";
import { MessageEmojiButton } from "src/components/Messages/MessageEmojiButton";
import { ReportPopup } from "src/components/Messages/ReportPopup";
Expand All @@ -46,23 +45,17 @@ import { Message, MessageEmojis } from "src/types/Conversation";
import { emojiIcons, isKnownEmoji } from "src/types/Emoji";
import { mutate } from "swr";
import useSWRMutation from "swr/mutation";

const RenderedMarkdown = lazy(() => import("./RenderedMarkdown"));

interface MessageTableEntryProps {
message: Message;
enabled?: boolean;
highlight?: boolean;
avartarPosition?: "middle" | "top";
avartarProps?: AvatarProps;
showAuthorBadge?: boolean;
}

export function MessageTableEntry({
message,
enabled,
highlight,
avartarPosition = "middle",
avartarProps,
showAuthorBadge,
}: MessageTableEntryProps) {
export function MessageTableEntry({ message, enabled, highlight, showAuthorBadge }: MessageTableEntryProps) {
const router = useRouter();
const [emojiState, setEmojis] = useState<MessageEmojis>({ emojis: {}, user_emojis: [] });
useEffect(() => {
Expand Down Expand Up @@ -94,12 +87,12 @@ export function MessageTableEntry({
borderColor={borderColor}
size={inlineAvatar ? "xs" : "sm"}
mr={inlineAvatar ? 2 : 0}
mt={inlineAvatar ? 0 : `6px`}
name={`${boolean(message.is_assistant) ? "Assistant" : "User"}`}
src={`${boolean(message.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`}
{...avartarProps}
/>
),
[avartarProps, borderColor, inlineAvatar, message.is_assistant]
[borderColor, inlineAvatar, message.is_assistant]
);
const highlightColor = useColorModeValue(colors.light.active, colors.dark.active);

Expand All @@ -118,11 +111,7 @@ export function MessageTableEntry({
const { t } = useTranslation(["message"]);

return (
<HStack
w={["full", "full", "full", "fit-content"]}
gap={0.5}
alignItems={avartarPosition === "top" ? "start" : "center"}
>
<HStack w={["full", "full", "full", "fit-content"]} gap={0.5} alignItems="start">
{!inlineAvatar && avatar}
<Box
width={["full", "full", "full", "fit-content"]}
Expand All @@ -133,12 +122,13 @@ export function MessageTableEntry({
outline={highlight ? "2px solid black" : undefined}
outlineColor={highlightColor}
onClick={goToMessage}
whiteSpace="pre-wrap"
cursor={enabled ? "pointer" : undefined}
style={{ position: "relative", wordBreak: "break-word" }}
style={{ position: "relative" }}
>
{inlineAvatar && avatar}
{message.text}
<Suspense fallback={message.text}>
<RenderedMarkdown markdown={message.text}></RenderedMarkdown>
</Suspense>
<HStack
style={{ float: "right", position: "relative", right: "-0.3em", bottom: "-0em", marginLeft: "1em" }}
onClick={(e) => e.stopPropagation()}
Expand Down
61 changes: 28 additions & 33 deletions website/src/components/Messages/MessageTree.tsx
Expand Up @@ -28,10 +28,6 @@ export const MessageTree = memo(({ tree, messageId }: { tree: MessageWithChildre
{hasChildren && depth < maxDepth && <Connection className="connection1"></Connection>}
<MessageTableEntry
showAuthorBadge
avartarProps={{
mt: { base: 0, md: `${avartarMarginTop}px` },
}}
avartarPosition="top"
highlight={child.id === messageId}
message={child}
></MessageTableEntry>
Expand All @@ -47,35 +43,34 @@ export const MessageTree = memo(({ tree, messageId }: { tree: MessageWithChildre
return (
<>
<Box position="relative" className="root">
<Box
className="root-connection"
top={{ base: toPx(0), md: 0 }}
height={{ base: `calc(100% - ${toPx(8 + avatarSize / 2)})`, md: `100%` }}
position="absolute"
width="2px"
bg={connectionColor}
left={toPx(avatarSize / 2 - 1)}
></Box>
<Box
display={{ base: "block", md: "none" }}
position="absolute"
height={`calc(100% - ${toPx(6 + avatarSize / 2)})`}
width={`10px`}
top={toPx(6 + avatarSize / 2)}
borderTopWidth="2px"
borderTopLeftRadius="10px"
left="-8px"
borderTopStyle="solid"
borderLeftWidth="2px"
borderColor={connectionColor}
className="root-curve"
></Box>
<MessageTableEntry
showAuthorBadge
message={tree}
avartarPosition="top"
highlight={tree.id === messageId}
></MessageTableEntry>
{tree.children.length > 0 && (
<>
<Box
className="root-connection"
top={{ base: toPx(0), md: toPx(8) }}
height={{ base: `calc(100% - ${toPx(8 + avatarSize / 2)})`, md: `calc(100% - 8px)` }}
position="absolute"
width="2px"
bg={connectionColor}
left={toPx(avatarSize / 2 - 1)}
></Box>
<Box
display={{ base: "block", md: "none" }}
position="absolute"
height={`calc(100% - ${toPx(6 + avatarSize / 2)})`}
width={`10px`}
top={toPx(6 + avatarSize / 2)}
borderTopWidth="2px"
borderTopLeftRadius="10px"
left="-8px"
borderTopStyle="solid"
borderLeftWidth="2px"
borderColor={connectionColor}
className="root-curve"
></Box>
</>
)}
<MessageTableEntry showAuthorBadge message={tree} highlight={tree.id === messageId}></MessageTableEntry>
</Box>
{renderChildren(tree.children)}
</>
Expand Down
87 changes: 87 additions & 0 deletions website/src/components/Messages/RenderedMarkdown.tsx
@@ -0,0 +1,87 @@
import { SystemStyleObject } from "@chakra-ui/react";
import { Prose } from "@nikolovlazar/chakra-ui-prose";
import { memo } from "react";
import { ReactMarkdown } from "react-markdown/lib/react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import remarkGfm from "remark-gfm";
interface RenderedMarkdownProps {
markdown: string;
}

const components = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
code({ node, inline, className, children, style, ...props }) {
const match = /language-(\w+)/.exec(className || "");
const lang = match ? match[1] : "";
return !inline ? (
<SyntaxHighlighter style={oneDark} language={lang} {...props}>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
};

const sx: SystemStyleObject = {
pre: {
bg: "transparent",
},
code: {
before: {
content: `""`, // charka prose come with "`" by default
},
bg: "gray.300",
p: 0.5,
borderRadius: "2px",
_dark: {
bg: "gray.700",
},
},
"p:only-child": {
my: 0, // ovoid margin when markdown only render 1 p tag
},
p: {
whiteSpace: "pre-wrap",
display: "inline-block",
},
display: "inline-block",
wordBreak: "break-word",
"> blockquote": {
borderInlineStartColor: "gray.300",
_dark: {
borderInlineStartColor: "gray.500",
},
},
"table tbody tr": {
borderBottomColor: "gray.400",
_dark: {
borderBottomColor: "gray.700",
},
},
"table thead tr": {
borderBottomColor: "gray.400",
borderBottomWidth: "1px",
_dark: {
borderBottomColor: "gray.700",
},
},
};

const plugins = [remarkGfm];

// eslint-disable-next-line react/display-name
const RenderedMarkdown = memo(({ markdown }: RenderedMarkdownProps) => {
return (
<Prose as="div" sx={sx}>
<ReactMarkdown remarkPlugins={plugins} components={components}>
{markdown}
</ReactMarkdown>
</Prose>
);
});

export default RenderedMarkdown;
10 changes: 8 additions & 2 deletions website/src/components/Sortable/Sortable.tsx
Expand Up @@ -25,11 +25,13 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { ReactNode, useEffect, useState } from "react";
import { lazy, ReactNode, Suspense, useEffect, useState } from "react";

import { CollapsableText } from "../CollapsableText";
import { SortableItem } from "./SortableItem";

const RenderedMarkdown = lazy(() => import("../Messages/RenderedMarkdown"));

export interface SortableProps {
items: ReactNode[];
onChange?: (newSortedIndices: number[]) => void;
Expand Down Expand Up @@ -110,7 +112,11 @@ export const Sortable = (props: SortableProps) => {
<ModalContent pb={5} alignItems="center">
<ModalHeader>Full Text</ModalHeader>
<ModalCloseButton />
<ModalBody whiteSpace="pre-line">{modalText}</ModalBody>
<ModalBody>
<Suspense fallback={modalText}>
<RenderedMarkdown markdown={modalText}></RenderedMarkdown>
</Suspense>
</ModalBody>
</ModalContent>
</ModalOverlay>
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion website/src/styles/Chakra.tsx
Expand Up @@ -6,7 +6,7 @@ export function Chakra({ cookies, children }) {
const colorModeManager = typeof cookies === "string" ? cookieStorageManagerSSR(cookies) : localStorageManager;

return (
<ChakraProvider theme={theme} colorModeManager={colorModeManager}>
<ChakraProvider theme={theme} colorModeManager={colorModeManager} resetCSS>
{children}
</ChakraProvider>
);
Expand Down
38 changes: 37 additions & 1 deletion website/src/styles/Theme/index.ts
@@ -1,5 +1,6 @@
import { type ThemeConfig, extendTheme } from "@chakra-ui/react";
import { Styles } from "@chakra-ui/theme-tools";
import { withProse } from "@nikolovlazar/chakra-ui-prose";

import { colors } from "./colors";
import { badgeTheme } from "./components/Badge";
Expand Down Expand Up @@ -50,4 +51,39 @@ const styles: Styles = {
}),
};

export const theme = extendTheme({ colors, config, fonts, styles, components, breakpoints });
export const theme = extendTheme(
{ colors, config, fonts, styles, components, breakpoints },
withProse({
baseStyle: {
"h1, h2, h3, h4, h5, h6": {
fontWeight: "500",
lineHeight: 1.2,
},
"h1, h2, h3": {
mt: 5,
mb: 2.5,
},
"h4, h5, h6": {
my: 2.5,
},
h1: {
fontSize: "2.5rem",
},
h2: {
fontSize: "2rem",
},
h3: {
fontSize: "1.75rem",
},
h4: {
fontSize: "1.5rem",
},
h5: {
fontSize: "1.25rem",
},
h6: {
fontSize: "1rem",
},
},
})
);
4 changes: 2 additions & 2 deletions website/src/styles/globals.css
@@ -1,4 +1,4 @@
@tailwind base;
/* @tailwind base; */
@tailwind components;
@tailwind utilities;

Expand All @@ -20,7 +20,7 @@
src: url("/fonts/Inter-italic.var.woff2") format("woff2");
}

@tailwind base;
/* @tailwind base; */
@tailwind components;
@tailwind utilities;

Expand Down

0 comments on commit 07bc2f6

Please sign in to comment.