mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
Remove unused components and assets from the project
Deleted unused components (Attachments, CustomMarkdownRenderer, EnableSearchButton, FlyoutSubMenu) and associated styles (katex.css) to streamline the codebase. This cleanup helps reduce technical debt and improve project maintainability.
This commit is contained in:

committed by
Geoff Seemueller

parent
d90ab65b04
commit
ceeefeff14
@@ -61,7 +61,6 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"katex": "^0.16.20",
|
||||
"lucide-react": "^0.436.0",
|
||||
"manifold-workflow-engine": "^2.0.2",
|
||||
"marked": "^15.0.4",
|
||||
"marked-extended-latex": "^1.1.0",
|
||||
"marked-footnote": "^1.2.4",
|
||||
@@ -75,15 +74,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-streaming": "^0.3.44",
|
||||
"react-textarea-autosize": "^8.5.5",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-react": "^8.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"shiki": "^1.24.0",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.7.2",
|
||||
|
@@ -4,9 +4,8 @@ import {
|
||||
welcome_home_text,
|
||||
welcome_home_tip,
|
||||
} from "../static-data/welcome_home_text";
|
||||
import CustomMarkdownRenderer, {
|
||||
WelcomeHomeMarkdownRenderer,
|
||||
} from "./chat/CustomMarkdownRenderer";
|
||||
import {renderMarkdown} from "./markdown/MarkdownComponent";
|
||||
|
||||
|
||||
function WelcomeHomeMessage({ visible }) {
|
||||
const containerVariants = {
|
||||
@@ -60,7 +59,7 @@ function WelcomeHomeMessage({ visible }) {
|
||||
>
|
||||
<Box userSelect={"none"}>
|
||||
<motion.div variants={textVariants}>
|
||||
<WelcomeHomeMarkdownRenderer markdown={welcome_home_text} />
|
||||
{renderMarkdown(welcome_home_text)}
|
||||
</motion.div>
|
||||
</Box>
|
||||
</motion.div>
|
||||
@@ -73,7 +72,7 @@ function WelcomeHomeMessage({ visible }) {
|
||||
color="text.secondary"
|
||||
mt={1}
|
||||
>
|
||||
<CustomMarkdownRenderer markdown={welcome_home_tip} />
|
||||
{renderMarkdown(welcome_home_tip)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</VStack>
|
@@ -1,40 +0,0 @@
|
||||
import React from "react";
|
||||
import { IconButton, Tag, TagCloseButton, TagLabel } from "@chakra-ui/react";
|
||||
import { PaperclipIcon } from "lucide-react";
|
||||
|
||||
// Add a new component for UploadedItem
|
||||
export const UploadedItem: React.FC<{
|
||||
url: string;
|
||||
onRemove: () => void;
|
||||
name: string;
|
||||
}> = ({ url, onRemove, name }) => (
|
||||
<Tag size="md" borderRadius="full" variant="solid" colorScheme="teal">
|
||||
<TagLabel>{name || url.split("/").pop()}</TagLabel>
|
||||
<TagCloseButton onClick={onRemove} />
|
||||
</Tag>
|
||||
);
|
||||
|
||||
export const AttachmentButton: React.FC<{
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
}> = ({ onClick, disabled }) => (
|
||||
<IconButton
|
||||
aria-label="Attach"
|
||||
title="Attach"
|
||||
bg="transparent"
|
||||
color="text.tertiary"
|
||||
icon={<PaperclipIcon size={"1.3337rem"} />}
|
||||
onClick={onClick}
|
||||
_hover={{
|
||||
bg: "transparent",
|
||||
svg: {
|
||||
stroke: "accent.secondary",
|
||||
transition: "stroke 0.3s ease-in-out",
|
||||
},
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
_focus={{ boxShadow: "none" }}
|
||||
/>
|
||||
);
|
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Box, Grid, GridItem } from "@chakra-ui/react";
|
||||
import ChatMessages from "./ChatMessages";
|
||||
import ChatInput from "./ChatInput";
|
||||
import ChatMessages from "./messages/ChatMessages";
|
||||
import ChatInput from "./input/ChatInput";
|
||||
import chatStore from "../../stores/ClientChatStore";
|
||||
import menuState from "../../stores/AppMenuStore";
|
||||
import WelcomeHomeMessage from "../WelcomeHomeMessage";
|
||||
import WelcomeHome from "../WelcomeHome";
|
||||
|
||||
const Chat = observer(({ height, width }) => {
|
||||
const scrollRef = useRef();
|
||||
@@ -26,7 +26,7 @@ const Chat = observer(({ height, width }) => {
|
||||
gap={0}
|
||||
>
|
||||
<GridItem alignSelf="center" hidden={!(chatStore.messages.length < 1)}>
|
||||
<WelcomeHomeMessage visible={chatStore.messages.length < 1} />
|
||||
<WelcomeHome visible={chatStore.messages.length < 1} />
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
import { renderCustomComponents } from "./renderCustomComponents";
|
||||
import { renderCustomComponents as renderWelcomeHomeMarkdown } from "./RenderWelcomeHomeCustomComponents";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
|
||||
interface CustomMarkdownRendererProps {
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
const CustomMarkdownRenderer: React.FC<CustomMarkdownRendererProps> = ({
|
||||
markdown,
|
||||
}) => {
|
||||
return <div>{renderCustomComponents(markdown)}</div>;
|
||||
};
|
||||
|
||||
export const WelcomeHomeMarkdownRenderer: React.FC<
|
||||
CustomMarkdownRendererProps
|
||||
> = ({ markdown }) => {
|
||||
return <Box color="text.accent">{renderWelcomeHomeMarkdown(markdown)}</Box>;
|
||||
};
|
||||
|
||||
export default CustomMarkdownRenderer;
|
@@ -1,49 +0,0 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import { Globe2Icon } from "lucide-react";
|
||||
import clientChatStore from "../../stores/ClientChatStore";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const EnableSearchButton: React.FC<{ disabled: boolean }> = observer(
|
||||
({ disabled }) => {
|
||||
const onClick = () => {
|
||||
if (clientChatStore.tools.includes("web-search")) {
|
||||
clientChatStore.setTools([]);
|
||||
} else {
|
||||
clientChatStore.setTools(["web-search"]);
|
||||
}
|
||||
};
|
||||
|
||||
const isActive = clientChatStore.tools.includes("web-search");
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={isActive ? "Disable Search" : "Enable Search"}
|
||||
title={isActive ? "Disable Search" : "Enable Search"}
|
||||
bg="transparent"
|
||||
color="text.tertiary"
|
||||
icon={<Globe2Icon size={"1.3337rem"} />}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
_active={{
|
||||
bg: "transparent",
|
||||
svg: {
|
||||
stroke: "brand.100",
|
||||
transition: "stroke 0.3s ease-in-out",
|
||||
},
|
||||
}}
|
||||
_hover={{
|
||||
bg: "transparent",
|
||||
svg: {
|
||||
stroke: "accent.secondary",
|
||||
transition: "stroke 0.3s ease-in-out",
|
||||
},
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
_focus={{ boxShadow: "none" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
@@ -1,135 +0,0 @@
|
||||
import React, { useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
HStack,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useIsMobile } from "../contexts/MobileContext";
|
||||
|
||||
const FlyoutSubMenu: React.FC<{
|
||||
title: string;
|
||||
flyoutMenuOptions: { name: string; value: string }[];
|
||||
onClose: () => void;
|
||||
handleSelect: (item) => Promise<void>;
|
||||
isSelected?: (item) => boolean;
|
||||
parentIsOpen: boolean;
|
||||
}> = observer(
|
||||
({
|
||||
title,
|
||||
flyoutMenuOptions,
|
||||
onClose,
|
||||
handleSelect,
|
||||
isSelected,
|
||||
parentIsOpen,
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose: onSubMenuClose } = useDisclosure();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const menuRef = new useRef();
|
||||
|
||||
useOutsideClick({
|
||||
ref: menuRef,
|
||||
enabled: !isMobile,
|
||||
handler: () => {
|
||||
onSubMenuClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu
|
||||
placement="right-start"
|
||||
isOpen={isOpen && parentIsOpen}
|
||||
closeOnBlur={true}
|
||||
onClose={() => {
|
||||
onSubMenuClose();
|
||||
}}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<MenuButton
|
||||
as={MenuItem}
|
||||
onClick={onOpen}
|
||||
ref={menuRef}
|
||||
bg="background.tertiary"
|
||||
color="text.primary"
|
||||
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
|
||||
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
<HStack width={"100%"} justifyContent={"space-between"}>
|
||||
<Text>{title}</Text>
|
||||
<ChevronRight size={"1rem"} />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<Portal>
|
||||
<MenuList
|
||||
key={title}
|
||||
maxHeight={56}
|
||||
overflowY="scroll"
|
||||
visibility={"visible"}
|
||||
minWidth="180px"
|
||||
bg="background.tertiary"
|
||||
boxShadow="lg"
|
||||
transform="translateY(-50%)"
|
||||
zIndex={9999}
|
||||
position="absolute"
|
||||
left="100%"
|
||||
bottom={-10}
|
||||
sx={{
|
||||
"::-webkit-scrollbar": {
|
||||
width: "8px",
|
||||
},
|
||||
"::-webkit-scrollbar-thumb": {
|
||||
background: "background.primary",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
"::-webkit-scrollbar-track": {
|
||||
background: "background.tertiary",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{flyoutMenuOptions.map((item, index) => (
|
||||
<Box key={"itemflybox" + index}>
|
||||
<MenuItem
|
||||
key={"itemfly" + index}
|
||||
onClick={() => {
|
||||
handleSelect(item);
|
||||
onSubMenuClose();
|
||||
onClose();
|
||||
}}
|
||||
bg={
|
||||
isSelected(item)
|
||||
? "background.secondary"
|
||||
: "background.tertiary"
|
||||
}
|
||||
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
|
||||
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
{index < flyoutMenuOptions.length - 1 && (
|
||||
<Divider
|
||||
key={item.name + "-divider"}
|
||||
color="text.tertiary"
|
||||
w={"100%"}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default FlyoutSubMenu;
|
@@ -1,156 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronDown, Copy, RefreshCcw, Settings } from "lucide-react";
|
||||
import ClientChatStore from "../../stores/ClientChatStore";
|
||||
import clientChatStore from "../../stores/ClientChatStore";
|
||||
import FlyoutSubMenu from "./FlyoutSubMenu";
|
||||
import { useIsMobile } from "../contexts/MobileContext";
|
||||
import { getModelFamily, SUPPORTED_MODELS } from "./SupportedModels";
|
||||
import { formatConversationMarkdown } from "./exportConversationAsMarkdown";
|
||||
|
||||
// Common styles for MenuButton and IconButton
|
||||
export const MsM_commonButtonStyles = {
|
||||
bg: "transparent",
|
||||
color: "text.primary",
|
||||
borderRadius: "full",
|
||||
padding: 2,
|
||||
border: "none",
|
||||
_hover: { bg: "rgba(255, 255, 255, 0.2)" },
|
||||
_active: { bg: "rgba(255, 255, 255, 0.3)" },
|
||||
_focus: { boxShadow: "none" },
|
||||
};
|
||||
|
||||
const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(
|
||||
({ isDisabled }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const textModels = SUPPORTED_MODELS;
|
||||
|
||||
const handleCopyConversation = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.writeText(formatConversationMarkdown(ClientChatStore.messages))
|
||||
.then(() => {
|
||||
window.alert(
|
||||
"Conversation copied to clipboard. \n\nPaste it somewhere safe!",
|
||||
);
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Could not copy text to clipboard: ", err);
|
||||
window.alert("Failed to copy conversation. Please try again.");
|
||||
});
|
||||
}, [onClose]);
|
||||
|
||||
async function selectModelFn({ name, value }) {
|
||||
if (getModelFamily(value)) {
|
||||
ClientChatStore.setModel(value);
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectedModelFn({ name, value }) {
|
||||
return ClientChatStore.model === value;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
closeOnSelect={false}
|
||||
closeOnBlur={true}
|
||||
>
|
||||
{isMobile ? (
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
bg="text.accent"
|
||||
icon={<Settings size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
aria-label="Settings"
|
||||
_hover={{ bg: "rgba(255, 255, 255, 0.2)" }}
|
||||
_focus={{ boxShadow: "none" }}
|
||||
{...MsM_commonButtonStyles}
|
||||
/>
|
||||
) : (
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronDown size={16} />}
|
||||
isDisabled={isDisabled}
|
||||
variant="ghost"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
minW="auto"
|
||||
{...MsM_commonButtonStyles}
|
||||
>
|
||||
<Text noOfLines={1} maxW="100px" fontSize="sm">
|
||||
{ClientChatStore.model}
|
||||
</Text>
|
||||
</MenuButton>
|
||||
)}
|
||||
<MenuList
|
||||
bg="background.tertiary"
|
||||
border="none"
|
||||
borderRadius="md"
|
||||
boxShadow="lg"
|
||||
minW={"10rem"}
|
||||
>
|
||||
<Divider color="text.tertiary" />
|
||||
<FlyoutSubMenu
|
||||
title="Text Models"
|
||||
flyoutMenuOptions={textModels.map((m) => ({ name: m, value: m }))}
|
||||
onClose={onClose}
|
||||
parentIsOpen={isOpen}
|
||||
handleSelect={selectModelFn}
|
||||
isSelected={isSelectedModelFn}
|
||||
/>
|
||||
<Divider color="text.tertiary" />
|
||||
{/*Export conversation button*/}
|
||||
<MenuItem
|
||||
bg="background.tertiary"
|
||||
color="text.primary"
|
||||
onClick={handleCopyConversation}
|
||||
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
|
||||
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Copy size="16px" style={{ marginRight: "8px" }} />
|
||||
<Box>Export</Box>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
{/*New conversation button*/}
|
||||
<MenuItem
|
||||
bg="background.tertiary"
|
||||
color="text.primary"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
clientChatStore.reset();
|
||||
}}
|
||||
_hover={{ bg: "rgba(0, 0, 0, 0.05)" }}
|
||||
_focus={{ bg: "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
<Flex align="center">
|
||||
<RefreshCcw size="16px" style={{ marginRight: "8px" }} />
|
||||
<Box>New</Box>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default InputMenu;
|
@@ -1,574 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Code,
|
||||
Divider,
|
||||
Heading,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { marked } from "marked";
|
||||
import CodeBlock from "../code/CodeBlock";
|
||||
import ImageWithFallback from "./ImageWithFallback";
|
||||
import markedKatex from "marked-katex-extension";
|
||||
import katex from "katex";
|
||||
|
||||
try {
|
||||
if (localStorage) {
|
||||
marked.use(
|
||||
markedKatex({
|
||||
nonStandard: false,
|
||||
displayMode: true,
|
||||
throwOnError: false,
|
||||
strict: true,
|
||||
colorIsTextColor: true,
|
||||
errorColor: "red",
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const MemoizedCodeBlock = React.memo(CodeBlock);
|
||||
|
||||
const getHeadingProps = (depth: number) => {
|
||||
switch (depth) {
|
||||
case 1:
|
||||
return { as: "h1", size: "xl", mt: 4, mb: 2 };
|
||||
case 2:
|
||||
return { as: "h2", size: "lg", mt: 3, mb: 2 };
|
||||
case 3:
|
||||
return { as: "h3", size: "md", mt: 2, mb: 1 };
|
||||
case 4:
|
||||
return { as: "h4", size: "sm", mt: 2, mb: 1 };
|
||||
case 5:
|
||||
return { as: "h5", size: "sm", mt: 2, mb: 1 };
|
||||
case 6:
|
||||
return { as: "h6", size: "xs", mt: 2, mb: 1 };
|
||||
default:
|
||||
return { as: `h${depth}`, size: "md", mt: 2, mb: 1 };
|
||||
}
|
||||
};
|
||||
|
||||
interface TableToken extends marked.Tokens.Table {
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
header: (string | marked.Tokens.TableCell)[];
|
||||
rows: (string | marked.Tokens.TableCell)[][];
|
||||
}
|
||||
|
||||
const CustomHeading: React.FC<{ text: string; depth: number }> = ({
|
||||
text,
|
||||
depth,
|
||||
}) => {
|
||||
const headingProps = getHeadingProps(depth);
|
||||
return (
|
||||
<Heading
|
||||
{...headingProps}
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
color="text.accent"
|
||||
>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Text
|
||||
as="p"
|
||||
fontSize="sm"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
as="blockquote"
|
||||
borderLeft="4px solid"
|
||||
borderColor="gray.200"
|
||||
fontStyle="italic"
|
||||
color="gray.600"
|
||||
pl={4}
|
||||
maxWidth="100%"
|
||||
wordBreak="break-word"
|
||||
mb={2}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({
|
||||
code,
|
||||
language,
|
||||
}) => {
|
||||
return (
|
||||
<MemoizedCodeBlock
|
||||
language={language}
|
||||
code={code}
|
||||
onRenderComplete={() => Promise.resolve()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomHr: React.FC = () => <Divider my={4} />;
|
||||
|
||||
const CustomList: React.FC<{
|
||||
ordered?: boolean;
|
||||
start?: number;
|
||||
children: React.ReactNode;
|
||||
}> = ({ ordered, start, children }) => {
|
||||
const commonStyles = {
|
||||
fontSize: "sm",
|
||||
wordBreak: "break-word" as const,
|
||||
maxWidth: "100%" as const,
|
||||
stylePosition: "outside" as const,
|
||||
mb: 2,
|
||||
pl: 4,
|
||||
};
|
||||
|
||||
return ordered ? (
|
||||
<OrderedList start={start} {...commonStyles}>
|
||||
{children}
|
||||
</OrderedList>
|
||||
) : (
|
||||
<List styleType="disc" {...commonStyles}>
|
||||
{children}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomListItem: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
return <ListItem mb={1}>{children}</ListItem>;
|
||||
};
|
||||
|
||||
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
|
||||
math,
|
||||
displayMode,
|
||||
}) => {
|
||||
const renderedMath = katex.renderToString(math, { displayMode });
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
// bg={bg}
|
||||
p={displayMode ? 4 : 1}
|
||||
my={displayMode ? 4 : 0}
|
||||
borderRadius="md"
|
||||
overflow="auto"
|
||||
maxWidth="100%"
|
||||
dangerouslySetInnerHTML={{ __html: renderedMath }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTable: React.FC<{
|
||||
header: React.ReactNode[];
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
rows: React.ReactNode[][];
|
||||
}> = ({ header, align, rows }) => {
|
||||
return (
|
||||
<Table
|
||||
variant="simple"
|
||||
size="sm"
|
||||
my={4}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Thead bg="background.secondary">
|
||||
<Tr>
|
||||
{header.map((cell, i) => (
|
||||
<Th
|
||||
key={i}
|
||||
textAlign={align[i] || "left"}
|
||||
fontWeight="bold"
|
||||
p={2}
|
||||
minW={16}
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{cell}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{rows.map((row, rIndex) => (
|
||||
<Tr key={rIndex}>
|
||||
{row.map((cell, cIndex) => (
|
||||
<Td
|
||||
key={cIndex}
|
||||
textAlign={align[cIndex] || "left"}
|
||||
p={2}
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{cell}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomHtmlBlock: React.FC<{ content: string }> = ({ content }) => {
|
||||
return <Box dangerouslySetInnerHTML={{ __html: content }} mb={2} />;
|
||||
};
|
||||
|
||||
const CustomText: React.FC<{ text: React.ReactNode }> = ({ text }) => {
|
||||
return (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
lineHeight="short"
|
||||
color="text.accent"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
as="span"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
interface CustomStrongProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const CustomStrong: React.FC<CustomStrongProps> = ({ children }) => {
|
||||
return <Text as="strong">{children}</Text>;
|
||||
};
|
||||
|
||||
const CustomEm: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Text
|
||||
as="em"
|
||||
fontStyle="italic"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
display="inline"
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomDel: React.FC<{ text: string }> = ({ text }) => {
|
||||
return (
|
||||
<Text
|
||||
as="del"
|
||||
textDecoration="line-through"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
display="inline"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeSpan: React.FC<{ code: string }> = ({ code }) => {
|
||||
const bg = useColorModeValue("gray.100", "gray.800");
|
||||
return (
|
||||
<Code
|
||||
fontSize="sm"
|
||||
bg={bg}
|
||||
overflowX="clip"
|
||||
borderRadius="md"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
p={0.5}
|
||||
>
|
||||
{code}
|
||||
</Code>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({
|
||||
math,
|
||||
displayMode = false,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
p={displayMode ? 4 : 1}
|
||||
my={displayMode ? 4 : 0}
|
||||
borderRadius="md"
|
||||
overflow="auto"
|
||||
maxWidth="100%"
|
||||
className={`math ${displayMode ? "math-display" : "math-inline"}`}
|
||||
>
|
||||
{math}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomLink: React.FC<{
|
||||
href: string;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ href, title, children, ...props }) => {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
title={title}
|
||||
isExternal
|
||||
sx={{
|
||||
"& span": {
|
||||
color: "text.link",
|
||||
},
|
||||
}}
|
||||
maxWidth="100%"
|
||||
color="teal.500"
|
||||
wordBreak="break-word"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomImage: React.FC<{ href: string; text: string; title?: string }> = ({
|
||||
href,
|
||||
text,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<ImageWithFallback
|
||||
src={href}
|
||||
alt={text}
|
||||
title={title}
|
||||
maxW="100%"
|
||||
width="auto"
|
||||
height="auto"
|
||||
my={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
const output: JSX.Element[] = [];
|
||||
let blockquoteContent: JSX.Element[] = [];
|
||||
|
||||
tokens.forEach((token, i) => {
|
||||
switch (token.type) {
|
||||
case "heading":
|
||||
output.push(
|
||||
<CustomHeading key={i} text={token.text} depth={token.depth} />,
|
||||
);
|
||||
break;
|
||||
|
||||
case "paragraph": {
|
||||
const parsedContent = token.tokens
|
||||
? parseTokens(token.tokens)
|
||||
: token.text;
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
} else {
|
||||
output.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "br":
|
||||
output.push(<br key={i} />);
|
||||
break;
|
||||
case "escape": {
|
||||
break;
|
||||
}
|
||||
case "blockquote_start":
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
|
||||
case "blockquote_end":
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{parseTokens(blockquoteContent)}
|
||||
</CustomBlockquote>,
|
||||
);
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
case "blockquote": {
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : null}
|
||||
</CustomBlockquote>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "math":
|
||||
output.push(
|
||||
<CustomMath key={i} math={(token as any).value} displayMode={true} />,
|
||||
);
|
||||
break;
|
||||
|
||||
case "inlineMath":
|
||||
output.push(
|
||||
<CustomMath
|
||||
key={i}
|
||||
math={(token as any).value}
|
||||
displayMode={false}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case "inlineKatex":
|
||||
case "blockKatex": {
|
||||
const katexToken = token as any;
|
||||
output.push(
|
||||
<CustomKatex
|
||||
key={i}
|
||||
math={katexToken.text}
|
||||
displayMode={katexToken.displayMode}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "code":
|
||||
output.push(
|
||||
<CustomCodeBlock key={i} code={token.text} language={token.lang} />,
|
||||
);
|
||||
break;
|
||||
|
||||
case "hr":
|
||||
output.push(<CustomHr key={i} />);
|
||||
break;
|
||||
case "list": {
|
||||
const { ordered, start, items } = token;
|
||||
const listItems = items.map((listItem, idx) => {
|
||||
const nestedContent = parseTokens(listItem.tokens);
|
||||
return <CustomListItem key={idx}>{nestedContent}</CustomListItem>;
|
||||
});
|
||||
|
||||
output.push(
|
||||
<CustomList key={i} ordered={ordered} start={start}>
|
||||
{listItems}
|
||||
</CustomList>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "table": {
|
||||
const tableToken = token as TableToken;
|
||||
|
||||
output.push(
|
||||
<CustomTable
|
||||
key={i}
|
||||
header={tableToken.header.map((cell) =>
|
||||
typeof cell === "string" ? cell : parseTokens(cell.tokens || []),
|
||||
)}
|
||||
align={tableToken.align}
|
||||
rows={tableToken.rows.map((row) =>
|
||||
row.map((cell) =>
|
||||
typeof cell === "string"
|
||||
? cell
|
||||
: parseTokens(cell.tokens || []),
|
||||
),
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "html":
|
||||
output.push(<CustomHtmlBlock key={i} content={token.text} />);
|
||||
break;
|
||||
case "def":
|
||||
case "space":
|
||||
break;
|
||||
case "strong":
|
||||
output.push(
|
||||
<CustomStrong key={i}>
|
||||
{parseTokens(token.tokens || [])}
|
||||
</CustomStrong>,
|
||||
);
|
||||
break;
|
||||
case "em":
|
||||
output.push(
|
||||
<CustomEm key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
</CustomEm>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "codespan":
|
||||
output.push(<CustomCodeSpan key={i} code={token.text} />);
|
||||
break;
|
||||
|
||||
case "link":
|
||||
output.push(
|
||||
<CustomLink key={i} href={token.href} title={token.title}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
</CustomLink>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "image":
|
||||
output.push(
|
||||
<CustomImage
|
||||
key={i}
|
||||
href={token.href}
|
||||
title={token.title}
|
||||
text={token.text}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "text": {
|
||||
const parsedContent = token.tokens
|
||||
? parseTokens(token.tokens)
|
||||
: token.text;
|
||||
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<React.Fragment key={i}>{parsedContent}</React.Fragment>,
|
||||
);
|
||||
} else {
|
||||
output.push(<CustomText key={i} text={parsedContent} />);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unhandled token type:", token.type, token);
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function renderCustomComponents(markdown: string): JSX.Element[] {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
silent: false,
|
||||
async: true,
|
||||
});
|
||||
|
||||
const tokens = marked.lexer(markdown);
|
||||
return parseTokens(tokens);
|
||||
}
|
@@ -20,8 +20,8 @@ import clientChatStore from "../../../stores/ClientChatStore";
|
||||
import FlyoutSubMenu from "./FlyoutSubMenu";
|
||||
import { useIsMobile } from "../../contexts/MobileContext";
|
||||
import { useIsMobile as useIsMobileUserAgent } from "../../../layout/_IsMobileHook";
|
||||
import { getModelFamily, SUPPORTED_MODELS } from "../SupportedModels";
|
||||
import { formatConversationMarkdown } from "../exportConversationAsMarkdown";
|
||||
import { getModelFamily, SUPPORTED_MODELS } from "../lib/SupportedModels";
|
||||
import { formatConversationMarkdown } from "../lib/exportConversationAsMarkdown";
|
||||
|
||||
export const MsM_commonButtonStyles = {
|
||||
bg: "transparent",
|
@@ -7,12 +7,12 @@ import {
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import chatStore from "../../stores/ClientChatStore";
|
||||
import InputMenu from "./flyoutmenu/InputMenu";
|
||||
import chatStore from "../../../stores/ClientChatStore";
|
||||
import InputMenu from "../input-menu/InputMenu";
|
||||
import InputTextarea from "./ChatInputTextArea";
|
||||
import SendButton from "./ChatInputSendButton";
|
||||
import { useMaxWidth } from "../../layout/useMaxWidth";
|
||||
import userOptionsStore from "../../stores/UserOptionsStore";
|
||||
import { useMaxWidth } from "../../../layout/useMaxWidth";
|
||||
import userOptionsStore from "../../../stores/UserOptionsStore";
|
||||
|
||||
const ChatInput = observer(() => {
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import clientChatStore from "../../stores/ClientChatStore";
|
||||
import clientChatStore from "../../../stores/ClientChatStore";
|
||||
import { CirclePause, Send } from "lucide-react";
|
||||
|
||||
import { motion } from "framer-motion";
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// Function to generate a Markdown representation of the current conversation
|
||||
import { type IMessage } from "../../stores/ClientChatStore";
|
||||
import { type IMessage } from "../../../stores/ClientChatStore";
|
||||
import { Instance } from "mobx-state-tree";
|
||||
|
||||
export function formatConversationMarkdown(
|
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import CustomMarkdownRenderer from "./CustomMarkdownRenderer";
|
||||
import MessageMarkdownRenderer from "./MessageMarkdownRenderer";
|
||||
|
||||
const ChatMessageContent = ({ content }) => {
|
||||
return <CustomMarkdownRenderer markdown={content} />;
|
||||
return <MessageMarkdownRenderer markdown={content} />;
|
||||
};
|
||||
|
||||
export default React.memo(ChatMessageContent);
|
@@ -2,8 +2,8 @@ import React from "react";
|
||||
import {Box, Grid, GridItem} from "@chakra-ui/react";
|
||||
import MessageBubble from "./MessageBubble";
|
||||
import {observer} from "mobx-react-lite";
|
||||
import chatStore from "../../stores/ClientChatStore";
|
||||
import {useIsMobile} from "../contexts/MobileContext";
|
||||
import chatStore from "../../../stores/ClientChatStore";
|
||||
import {useIsMobile} from "../../contexts/MobileContext";
|
||||
|
||||
interface ChatMessagesProps {
|
||||
scrollRef: React.RefObject<HTMLDivElement>;
|
@@ -3,11 +3,10 @@ import { motion } from "framer-motion";
|
||||
import { Box, Flex, Text } from "@chakra-ui/react";
|
||||
import MessageRenderer from "./ChatMessageContent";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { IntermediateStepsComponent } from "./IntermediateStepsComponent";
|
||||
import MessageEditor from "./MessageEditorComponent";
|
||||
import UserMessageTools from "./UserMessageTools";
|
||||
import clientChatStore from "../../stores/ClientChatStore";
|
||||
import UserOptionsStore from "../../stores/UserOptionsStore";
|
||||
import clientChatStore from "../../../stores/ClientChatStore";
|
||||
import UserOptionsStore from "../../../stores/UserOptionsStore";
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
@@ -128,7 +127,6 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IntermediateStepsComponent hidden={msg.role === "user"} />
|
||||
{isEditing ? (
|
||||
<MessageEditor message={msg} onCancel={handleCancelEdit} />
|
||||
) : isLoading ? (
|
@@ -2,7 +2,7 @@ import React, { KeyboardEvent, useState } from "react";
|
||||
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import store, { type IMessage } from "../../stores/ClientChatStore";
|
||||
import store, { type IMessage } from "../../../stores/ClientChatStore";
|
||||
|
||||
interface MessageEditorProps {
|
||||
message: IMessage;
|
@@ -19,10 +19,11 @@ import {
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { marked } from "marked";
|
||||
import CodeBlock from "../code/CodeBlock";
|
||||
import ImageWithFallback from "./ImageWithFallback";
|
||||
import CodeBlock from "../../code/CodeBlock";
|
||||
import ImageWithFallback from "../../markdown/ImageWithFallback";
|
||||
import markedKatex from "marked-katex-extension";
|
||||
import katex from "katex";
|
||||
import domPurify from "../lib/domPurify";
|
||||
|
||||
try {
|
||||
if (localStorage) {
|
||||
@@ -563,7 +564,7 @@ function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function renderCustomComponents(markdown: string): JSX.Element[] {
|
||||
export function renderMessageMarkdown(markdown: string): JSX.Element[] {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
@@ -571,6 +572,6 @@ export function renderCustomComponents(markdown: string): JSX.Element[] {
|
||||
async: true,
|
||||
});
|
||||
|
||||
const tokens = marked.lexer(markdown);
|
||||
const tokens = marked.lexer(domPurify(markdown));
|
||||
return parseTokens(tokens);
|
||||
}
|
14
src/components/chat/messages/MessageMarkdownRenderer.tsx
Normal file
14
src/components/chat/messages/MessageMarkdownRenderer.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import {renderMessageMarkdown} from "./MessageMarkdown";
|
||||
|
||||
interface CustomMarkdownRendererProps {
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
const MessageMarkdownRenderer: React.FC<CustomMarkdownRendererProps> = ({
|
||||
markdown,
|
||||
}) => {
|
||||
return <div>{renderMessageMarkdown(markdown)}</div>;
|
||||
};
|
||||
|
||||
export default MessageMarkdownRenderer;
|
@@ -1,19 +0,0 @@
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
export default function remarkImageGeneration() {
|
||||
return (tree) => {
|
||||
visit(tree, "code", (node, index, parent) => {
|
||||
if (node.lang === "generation") {
|
||||
try {
|
||||
const data = JSON.parse(node.value);
|
||||
parent.children[index] = {
|
||||
type: "generation",
|
||||
data: data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Invalid JSON in image-generation block:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { Box, VStack } from "@chakra-ui/react";
|
||||
import Markdown from "react-markdown";
|
||||
import { webComponents } from "../react-markdown/WebComponents";
|
||||
import {renderMarkdown} from "../markdown/MarkdownComponent";
|
||||
|
||||
function LegalDoc({ text }) {
|
||||
return (
|
||||
@@ -13,7 +12,7 @@ function LegalDoc({ text }) {
|
||||
whiteSpace="pre-wrap"
|
||||
spacing={4}
|
||||
>
|
||||
<Markdown components={webComponents}>{text}</Markdown>
|
||||
{renderMarkdown(text)}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
576
src/components/markdown/MarkdownComponent.tsx
Normal file
576
src/components/markdown/MarkdownComponent.tsx
Normal file
@@ -0,0 +1,576 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Code,
|
||||
Divider,
|
||||
Heading,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import {marked} from "marked";
|
||||
|
||||
import markedKatex from "marked-katex-extension";
|
||||
import katex from "katex";
|
||||
import CodeBlock from "../code/CodeBlock";
|
||||
import ImageWithFallback from "./ImageWithFallback";
|
||||
|
||||
try {
|
||||
if (localStorage) {
|
||||
marked.use(
|
||||
markedKatex({
|
||||
nonStandard: false,
|
||||
displayMode: true,
|
||||
throwOnError: false,
|
||||
strict: true,
|
||||
colorIsTextColor: true,
|
||||
errorColor: "red",
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
}
|
||||
|
||||
const MemoizedCodeBlock = React.memo(CodeBlock);
|
||||
|
||||
const getHeadingProps = (depth: number) => {
|
||||
switch (depth) {
|
||||
case 1:
|
||||
return {as: "h1", size: "xl", mt: 4, mb: 2};
|
||||
case 2:
|
||||
return {as: "h2", size: "lg", mt: 3, mb: 2};
|
||||
case 3:
|
||||
return {as: "h3", size: "md", mt: 2, mb: 1};
|
||||
case 4:
|
||||
return {as: "h4", size: "sm", mt: 2, mb: 1};
|
||||
case 5:
|
||||
return {as: "h5", size: "sm", mt: 2, mb: 1};
|
||||
case 6:
|
||||
return {as: "h6", size: "xs", mt: 2, mb: 1};
|
||||
default:
|
||||
return {as: `h${depth}`, size: "md", mt: 2, mb: 1};
|
||||
}
|
||||
};
|
||||
|
||||
interface TableToken extends marked.Tokens.Table {
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
header: (string | marked.Tokens.TableCell)[];
|
||||
rows: (string | marked.Tokens.TableCell)[][];
|
||||
}
|
||||
|
||||
const CustomHeading: React.FC<{ text: string; depth: number }> = ({
|
||||
text,
|
||||
depth,
|
||||
}) => {
|
||||
const headingProps = getHeadingProps(depth);
|
||||
return (
|
||||
<Heading
|
||||
{...headingProps}
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
color="text.accent"
|
||||
>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Text
|
||||
as="p"
|
||||
fontSize="sm"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
as="blockquote"
|
||||
borderLeft="4px solid"
|
||||
borderColor="gray.200"
|
||||
fontStyle="italic"
|
||||
color="gray.600"
|
||||
pl={4}
|
||||
maxWidth="100%"
|
||||
wordBreak="break-word"
|
||||
mb={2}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({
|
||||
code,
|
||||
language,
|
||||
}) => {
|
||||
return (
|
||||
<MemoizedCodeBlock
|
||||
language={language}
|
||||
code={code}
|
||||
onRenderComplete={() => Promise.resolve()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomHr: React.FC = () => <Divider my={4}/>;
|
||||
|
||||
const CustomList: React.FC<{
|
||||
ordered?: boolean;
|
||||
start?: number;
|
||||
children: React.ReactNode;
|
||||
}> = ({ordered, start, children}) => {
|
||||
const commonStyles = {
|
||||
fontSize: "sm",
|
||||
wordBreak: "break-word" as const,
|
||||
maxWidth: "100%" as const,
|
||||
stylePosition: "outside" as const,
|
||||
mb: 2,
|
||||
pl: 4,
|
||||
};
|
||||
|
||||
return ordered ? (
|
||||
<OrderedList start={start} {...commonStyles}>
|
||||
{children}
|
||||
</OrderedList>
|
||||
) : (
|
||||
<List styleType="disc" {...commonStyles}>
|
||||
{children}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomListItem: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({children}) => {
|
||||
return <ListItem mb={1}>{children}</ListItem>;
|
||||
};
|
||||
|
||||
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
|
||||
math,
|
||||
displayMode,
|
||||
}) => {
|
||||
const renderedMath = katex.renderToString(math, {displayMode});
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
// bg={bg}
|
||||
p={displayMode ? 4 : 1}
|
||||
my={displayMode ? 4 : 0}
|
||||
borderRadius="md"
|
||||
overflow="auto"
|
||||
maxWidth="100%"
|
||||
dangerouslySetInnerHTML={{__html: renderedMath}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTable: React.FC<{
|
||||
header: React.ReactNode[];
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
rows: React.ReactNode[][];
|
||||
}> = ({header, align, rows}) => {
|
||||
return (
|
||||
<Table
|
||||
variant="simple"
|
||||
size="sm"
|
||||
my={4}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Thead bg="background.secondary">
|
||||
<Tr>
|
||||
{header.map((cell, i) => (
|
||||
<Th
|
||||
key={i}
|
||||
textAlign={align[i] || "left"}
|
||||
fontWeight="bold"
|
||||
p={2}
|
||||
minW={16}
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{cell}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{rows.map((row, rIndex) => (
|
||||
<Tr key={rIndex}>
|
||||
{row.map((cell, cIndex) => (
|
||||
<Td
|
||||
key={cIndex}
|
||||
textAlign={align[cIndex] || "left"}
|
||||
p={2}
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{cell}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomHtmlBlock: React.FC<{ content: string }> = ({content}) => {
|
||||
return <Box dangerouslySetInnerHTML={{__html: content}} mb={2}/>;
|
||||
};
|
||||
|
||||
const CustomText: React.FC<{ text: React.ReactNode }> = ({text}) => {
|
||||
return (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
lineHeight="short"
|
||||
color="text.accent"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
as="span"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
interface CustomStrongProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const CustomStrong: React.FC<CustomStrongProps> = ({children}) => {
|
||||
return <Text as="strong">{children}</Text>;
|
||||
};
|
||||
|
||||
const CustomEm: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
return (
|
||||
<Text
|
||||
as="em"
|
||||
fontStyle="italic"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
display="inline"
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomDel: React.FC<{ text: string }> = ({text}) => {
|
||||
return (
|
||||
<Text
|
||||
as="del"
|
||||
textDecoration="line-through"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
display="inline"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeSpan: React.FC<{ code: string }> = ({code}) => {
|
||||
const bg = useColorModeValue("gray.100", "gray.800");
|
||||
return (
|
||||
<Code
|
||||
fontSize="sm"
|
||||
bg={bg}
|
||||
overflowX="clip"
|
||||
borderRadius="md"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
p={0.5}
|
||||
>
|
||||
{code}
|
||||
</Code>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({
|
||||
math,
|
||||
displayMode = false,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
p={displayMode ? 4 : 1}
|
||||
my={displayMode ? 4 : 0}
|
||||
borderRadius="md"
|
||||
overflow="auto"
|
||||
maxWidth="100%"
|
||||
className={`math ${displayMode ? "math-display" : "math-inline"}`}
|
||||
>
|
||||
{math}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomLink: React.FC<{
|
||||
href: string;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({href, title, children, ...props}) => {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
title={title}
|
||||
isExternal
|
||||
sx={{
|
||||
"& span": {
|
||||
color: "text.link",
|
||||
},
|
||||
}}
|
||||
maxWidth="100%"
|
||||
color="teal.500"
|
||||
wordBreak="break-word"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomImage: React.FC<{ href: string; text: string; title?: string }> = ({
|
||||
href,
|
||||
text,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<ImageWithFallback
|
||||
src={href}
|
||||
alt={text}
|
||||
title={title}
|
||||
maxW="100%"
|
||||
width="auto"
|
||||
height="auto"
|
||||
my={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
const output: JSX.Element[] = [];
|
||||
let blockquoteContent: JSX.Element[] = [];
|
||||
|
||||
tokens.forEach((token, i) => {
|
||||
switch (token.type) {
|
||||
case "heading":
|
||||
output.push(
|
||||
<CustomHeading key={i} text={token.text} depth={token.depth}/>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "paragraph": {
|
||||
const parsedContent = token.tokens
|
||||
? parseTokens(token.tokens)
|
||||
: token.text;
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
} else {
|
||||
output.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "br":
|
||||
output.push(<br key={i}/>);
|
||||
break;
|
||||
case "escape": {
|
||||
break;
|
||||
}
|
||||
case "blockquote_start":
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
|
||||
case "blockquote_end":
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{parseTokens(blockquoteContent)}
|
||||
</CustomBlockquote>,
|
||||
);
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
case "blockquote": {
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : null}
|
||||
</CustomBlockquote>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "math":
|
||||
output.push(
|
||||
<CustomMath key={i} math={(token as any).value} displayMode={true}/>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "inlineMath":
|
||||
output.push(
|
||||
<CustomMath
|
||||
key={i}
|
||||
math={(token as any).value}
|
||||
displayMode={false}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case "inlineKatex":
|
||||
case "blockKatex": {
|
||||
const katexToken = token as any;
|
||||
output.push(
|
||||
<CustomKatex
|
||||
key={i}
|
||||
math={katexToken.text}
|
||||
displayMode={katexToken.displayMode}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "code":
|
||||
output.push(
|
||||
<CustomCodeBlock key={i} code={token.text} language={token.lang}/>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "hr":
|
||||
output.push(<CustomHr key={i}/>);
|
||||
break;
|
||||
case "list": {
|
||||
const {ordered, start, items} = token;
|
||||
const listItems = items.map((listItem, idx) => {
|
||||
const nestedContent = parseTokens(listItem.tokens);
|
||||
return <CustomListItem key={idx}>{nestedContent}</CustomListItem>;
|
||||
});
|
||||
|
||||
output.push(
|
||||
<CustomList key={i} ordered={ordered} start={start}>
|
||||
{listItems}
|
||||
</CustomList>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "table": {
|
||||
const tableToken = token as TableToken;
|
||||
|
||||
output.push(
|
||||
<CustomTable
|
||||
key={i}
|
||||
header={tableToken.header.map((cell) =>
|
||||
typeof cell === "string" ? cell : parseTokens(cell.tokens || []),
|
||||
)}
|
||||
align={tableToken.align}
|
||||
rows={tableToken.rows.map((row) =>
|
||||
row.map((cell) =>
|
||||
typeof cell === "string"
|
||||
? cell
|
||||
: parseTokens(cell.tokens || []),
|
||||
),
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "html":
|
||||
output.push(<CustomHtmlBlock key={i} content={token.text}/>);
|
||||
break;
|
||||
case "def":
|
||||
case "space":
|
||||
break;
|
||||
case "strong":
|
||||
output.push(
|
||||
<CustomStrong key={i}>
|
||||
{parseTokens(token.tokens || [])}
|
||||
</CustomStrong>,
|
||||
);
|
||||
break;
|
||||
case "em":
|
||||
output.push(
|
||||
<CustomEm key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
</CustomEm>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "codespan":
|
||||
output.push(<CustomCodeSpan key={i} code={token.text}/>);
|
||||
break;
|
||||
|
||||
case "link":
|
||||
output.push(
|
||||
<CustomLink key={i} href={token.href} title={token.title}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
</CustomLink>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "image":
|
||||
output.push(
|
||||
<CustomImage
|
||||
key={i}
|
||||
href={token.href}
|
||||
title={token.title}
|
||||
text={token.text}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "text": {
|
||||
const parsedContent = token.tokens
|
||||
? parseTokens(token.tokens)
|
||||
: token.text;
|
||||
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<React.Fragment key={i}>{parsedContent}</React.Fragment>,
|
||||
);
|
||||
} else {
|
||||
output.push(<CustomText key={i} text={parsedContent}/>);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unhandled token type:", token.type, token);
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function renderMarkdown(markdown: string): JSX.Element[] {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
silent: false,
|
||||
async: true,
|
||||
});
|
||||
|
||||
const tokens = marked.lexer(markdown);
|
||||
return parseTokens(tokens);
|
||||
}
|
@@ -1,123 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Heading,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
Text,
|
||||
UnorderedList,
|
||||
} from "@chakra-ui/react";
|
||||
import ImageWithFallback from "../chat/ImageWithFallback";
|
||||
import { MdCheckCircle } from "react-icons/md";
|
||||
|
||||
export const webComponents = {
|
||||
p: ({ children }) => (
|
||||
<Text fontSize="sm" lineHeight="short">
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
strong: ({ children }) => <strong>{children}</strong>,
|
||||
h1: ({ children }) => (
|
||||
<Heading as="h1" size="xl" mt={4} mb={2}>
|
||||
{children}
|
||||
</Heading>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<Heading as="h2" size="lg" mt={3} mb={2}>
|
||||
{children}
|
||||
</Heading>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<Heading as="h3" size="md" mt={2} mb={1}>
|
||||
{children}
|
||||
</Heading>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<Heading as="h4" size="sm" mt={2} mb={1}>
|
||||
{children}
|
||||
</Heading>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<UnorderedList
|
||||
// pl={3}
|
||||
// mb={2}
|
||||
fontSize="sm"
|
||||
// stylePosition="inside" // Keep bullets inside the text flow
|
||||
>
|
||||
{children}
|
||||
</UnorderedList>
|
||||
),
|
||||
|
||||
ol: ({ children }) => (
|
||||
<OrderedList fontSize="sm" spacing={2}>
|
||||
{children}
|
||||
</OrderedList>
|
||||
),
|
||||
li: ({ children, ...rest }) => {
|
||||
const filteredChildren = React.Children.toArray(children)
|
||||
.filter((child) => !(typeof child === "string" && child.trim() === "\n"))
|
||||
.map((child, index, array) => {
|
||||
// if (typeof child === 'string' && index === array.length - 1 && /\n/.test(child)) {
|
||||
// return '\n';
|
||||
// }
|
||||
return child;
|
||||
});
|
||||
|
||||
return <ListItem {...rest}>{filteredChildren}</ListItem>;
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<Box
|
||||
as="pre"
|
||||
whiteSpace="pre-wrap"
|
||||
bg="background.secondary"
|
||||
borderRadius="md"
|
||||
fontSize="sm"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
|
||||
blockquote: ({ children }) => (
|
||||
<Box
|
||||
as="blockquote"
|
||||
borderLeft="4px solid"
|
||||
borderColor="gray.200"
|
||||
fontStyle="italic"
|
||||
color="gray.600"
|
||||
pl={4}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
hr: () => <Divider my={4} />,
|
||||
a: ({ href, children }) => (
|
||||
<Link
|
||||
color="teal.500"
|
||||
href={href}
|
||||
isExternal
|
||||
maxWidth="100%"
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
img: ({ alt, src }) => <ImageWithFallback alt={alt} src={src} />,
|
||||
icon_list: ({ children }) => (
|
||||
<List spacing={3}>
|
||||
{React.Children.map(children, (child) => (
|
||||
<ListItem>
|
||||
<Box
|
||||
as={MdCheckCircle}
|
||||
color="green.500"
|
||||
mr={2}
|
||||
display="inline-block"
|
||||
/>
|
||||
{child}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
),
|
||||
};
|
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
||||
import { Stack } from "@chakra-ui/react";
|
||||
import Chat from "../../components/chat/Chat";
|
||||
import clientChatStore from "../../stores/ClientChatStore";
|
||||
import { getModelFamily } from "../../components/chat/SupportedModels";
|
||||
import { getModelFamily } from "../../components/chat/lib/SupportedModels";
|
||||
|
||||
// renders "/"
|
||||
export default function IndexPage() {
|
||||
|
@@ -2,7 +2,7 @@ import {OpenAI} from "openai";
|
||||
import Message from "../models/Message";
|
||||
import {AssistantSdk} from "./assistant-sdk";
|
||||
import {IMessage} from "../../../src/stores/ClientChatStore";
|
||||
import {getModelFamily} from "../../../src/components/chat/SupportedModels";
|
||||
import {getModelFamily} from "../../../src/components/chat/lib/SupportedModels";
|
||||
|
||||
export class ChatSdk {
|
||||
static async preprocess({
|
||||
|
@@ -3,7 +3,7 @@ import OpenAI from 'openai';
|
||||
import ChatSdk from '../sdk/chat-sdk';
|
||||
import Message from "../models/Message";
|
||||
import O1Message from "../models/O1Message";
|
||||
import {getModelFamily, ModelFamily} from "../../../src/components/chat/SupportedModels";
|
||||
import {getModelFamily, ModelFamily} from "../../../src/components/chat/lib/SupportedModels";
|
||||
import {OpenAiChatSdk} from "../sdk/models/openai";
|
||||
import {GroqChatSdk} from "../sdk/models/groq";
|
||||
import {ClaudeChatSdk} from "../sdk/models/claude";
|
||||
|
Reference in New Issue
Block a user