mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
adds eslint
This commit is contained in:

committed by
Geoff Seemueller

parent
9698fc6f3b
commit
02c3253343
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import MessageMarkdownRenderer from "./MessageMarkdownRenderer";
|
||||
import MessageMarkdownRenderer from './MessageMarkdownRenderer';
|
||||
|
||||
const ChatMessageContent = ({ content }) => {
|
||||
return <MessageMarkdownRenderer markdown={content} />;
|
||||
|
@@ -1,9 +1,11 @@
|
||||
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 { Box, Grid, GridItem } from '@chakra-ui/react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
import chatStore from '../../../stores/ClientChatStore';
|
||||
import { useIsMobile } from '../../contexts/MobileContext';
|
||||
|
||||
import MessageBubble from './MessageBubble';
|
||||
|
||||
interface ChatMessagesProps {
|
||||
scrollRef: React.RefObject<HTMLDivElement>;
|
||||
@@ -13,11 +15,7 @@ const ChatMessages: React.FC<ChatMessagesProps> = observer(({ scrollRef }) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Box
|
||||
pt={isMobile ? 24 : undefined}
|
||||
overflowY={"scroll"}
|
||||
overflowX={"hidden"}
|
||||
>
|
||||
<Box pt={isMobile ? 24 : undefined} overflowY={'scroll'} overflowX={'hidden'}>
|
||||
<Grid
|
||||
fontFamily="Arial, sans-serif"
|
||||
templateColumns="1fr"
|
||||
|
@@ -1,43 +1,43 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Box, Flex, Text } from "@chakra-ui/react";
|
||||
import MessageRenderer from "./ChatMessageContent";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import MessageEditor from "./MessageEditorComponent";
|
||||
import UserMessageTools from "./UserMessageTools";
|
||||
import clientChatStore from "../../../stores/ClientChatStore";
|
||||
import UserOptionsStore from "../../../stores/UserOptionsStore";
|
||||
import MotionBox from "./MotionBox";
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import clientChatStore from '../../../stores/ClientChatStore';
|
||||
import UserOptionsStore from '../../../stores/UserOptionsStore';
|
||||
|
||||
import MessageRenderer from './ChatMessageContent';
|
||||
import MessageEditor from './MessageEditorComponent';
|
||||
import MotionBox from './MotionBox';
|
||||
import UserMessageTools from './UserMessageTools';
|
||||
|
||||
const LoadingDots = () => {
|
||||
return (
|
||||
<Flex>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<MotionBox
|
||||
key={i}
|
||||
width="8px"
|
||||
height="8px"
|
||||
borderRadius="50%"
|
||||
backgroundColor="text.primary"
|
||||
margin="0 4px"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex>
|
||||
{[0, 1, 2].map(i => (
|
||||
<MotionBox
|
||||
key={i}
|
||||
width="8px"
|
||||
height="8px"
|
||||
borderRadius="50%"
|
||||
backgroundColor="text.primary"
|
||||
margin="0 4px"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function renderMessage(msg: any) {
|
||||
if (msg.role === "user") {
|
||||
if (msg.role === 'user') {
|
||||
return (
|
||||
<Text as="p" fontSize="sm" lineHeight="short" color="text.primary">
|
||||
{msg.content}
|
||||
@@ -50,8 +50,8 @@ function renderMessage(msg: any) {
|
||||
const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isUser = msg.role === "user";
|
||||
const senderName = isUser ? "You" : "Geoff's AI";
|
||||
const isUser = msg.role === 'user';
|
||||
const senderName = isUser ? 'You' : "Geoff's AI";
|
||||
const isLoading = !msg.content || !(msg.content.trim().length > 0);
|
||||
const messageRef = useRef();
|
||||
|
||||
@@ -64,10 +64,15 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (clientChatStore.items.length > 0 && clientChatStore.isLoading && UserOptionsStore.followModeEnabled) { // Refine condition
|
||||
if (
|
||||
clientChatStore.items.length > 0 &&
|
||||
clientChatStore.isLoading &&
|
||||
UserOptionsStore.followModeEnabled
|
||||
) {
|
||||
// Refine condition
|
||||
scrollRef.current?.scrollTo({
|
||||
top: scrollRef.current.scrollHeight,
|
||||
behavior: "auto",
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -75,7 +80,7 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems={isUser ? "flex-end" : "flex-start"}
|
||||
alignItems={isUser ? 'flex-end' : 'flex-start'}
|
||||
role="listitem"
|
||||
flex={0}
|
||||
aria-label={`Message from ${senderName}`}
|
||||
@@ -85,19 +90,19 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color="text.tertiary"
|
||||
textAlign={isUser ? "right" : "left"}
|
||||
alignSelf={isUser ? "flex-end" : "flex-start"}
|
||||
textAlign={isUser ? 'right' : 'left'}
|
||||
alignSelf={isUser ? 'flex-end' : 'flex-start'}
|
||||
mb={1}
|
||||
>
|
||||
{senderName}
|
||||
</Text>
|
||||
|
||||
<MotionBox
|
||||
minW={{ base: "99%", sm: "99%", lg: isUser ? "55%" : "60%" }}
|
||||
maxW={{ base: "99%", sm: "99%", lg: isUser ? "65%" : "65%" }}
|
||||
minW={{ base: '99%', sm: '99%', lg: isUser ? '55%' : '60%' }}
|
||||
maxW={{ base: '99%', sm: '99%', lg: isUser ? '65%' : '65%' }}
|
||||
p={3}
|
||||
borderRadius="1.5em"
|
||||
bg={isUser ? "#0A84FF" : "#3A3A3C"}
|
||||
bg={isUser ? '#0A84FF' : '#3A3A3C'}
|
||||
color="text.primary"
|
||||
textAlign="left"
|
||||
boxShadow="0 2px 4px rgba(0, 0, 0, 0.1)"
|
||||
@@ -115,10 +120,10 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
whiteSpace="pre-wrap"
|
||||
ref={messageRef}
|
||||
sx={{
|
||||
"pre, code": {
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowX: "auto",
|
||||
'pre, code': {
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'auto',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -139,9 +144,7 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
{isHovered && !isEditing && (
|
||||
<UserMessageTools message={msg} onEdit={handleEdit} />
|
||||
)}
|
||||
{isHovered && !isEditing && <UserMessageTools message={msg} onEdit={handleEdit} />}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import React, {type KeyboardEvent, useEffect } from "react";
|
||||
import { Box, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { type Instance } from "mobx-state-tree";
|
||||
import Message from "../../../models/Message";
|
||||
import messageEditorStore from "../../../stores/MessageEditorStore";
|
||||
import { Box, Flex, IconButton, Textarea } from '@chakra-ui/react';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { type Instance } from 'mobx-state-tree';
|
||||
import React, { type KeyboardEvent, useEffect } from 'react';
|
||||
|
||||
import Message from '../../../models/Message';
|
||||
import messageEditorStore from '../../../stores/MessageEditorStore';
|
||||
|
||||
interface MessageEditorProps {
|
||||
message: Instance<typeof Message>;
|
||||
@@ -30,15 +31,13 @@ const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
@@ -48,14 +47,14 @@ const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
||||
<Box width="100%">
|
||||
<Textarea
|
||||
value={messageEditorStore.editedContent}
|
||||
onChange={(e) => messageEditorStore.setEditedContent(e.target.value)}
|
||||
onChange={e => messageEditorStore.setEditedContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
minHeight="100px"
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.300"
|
||||
_hover={{ borderColor: "whiteAlpha.400" }}
|
||||
_focus={{ borderColor: "brand.100", boxShadow: "none" }}
|
||||
_hover={{ borderColor: 'whiteAlpha.400' }}
|
||||
_focus={{ borderColor: 'brand.100', boxShadow: 'none' }}
|
||||
resize="vertical"
|
||||
color="text.primary"
|
||||
/>
|
||||
@@ -66,7 +65,7 @@ const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
||||
onClick={handleCancel}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color={"accent.danger"}
|
||||
color={'accent.danger'}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Save edit"
|
||||
@@ -74,7 +73,7 @@ const MessageEditor = observer(({ message, onCancel }: MessageEditorProps) => {
|
||||
onClick={handleSave}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color={"accent.confirm"}
|
||||
color={'accent.confirm'}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Code,
|
||||
@@ -17,13 +15,15 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { marked } from "marked";
|
||||
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";
|
||||
} from '@chakra-ui/react';
|
||||
import katex from 'katex';
|
||||
import { marked } from 'marked';
|
||||
import markedKatex from 'marked-katex-extension';
|
||||
import React from 'react';
|
||||
|
||||
import CodeBlock from '../../code/CodeBlock';
|
||||
import ImageWithFallback from '../../markdown/ImageWithFallback';
|
||||
import domPurify from '../lib/domPurify';
|
||||
|
||||
try {
|
||||
if (localStorage) {
|
||||
@@ -34,11 +34,13 @@ try {
|
||||
throwOnError: false,
|
||||
strict: true,
|
||||
colorIsTextColor: true,
|
||||
errorColor: "red",
|
||||
errorColor: 'red',
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
// Silently ignore errors in marked setup - fallback to default behavior
|
||||
}
|
||||
|
||||
const MemoizedCodeBlock = React.memo(CodeBlock);
|
||||
|
||||
@@ -49,32 +51,29 @@ const MemoizedCodeBlock = React.memo(CodeBlock);
|
||||
const getHeadingProps = (depth: number) => {
|
||||
switch (depth) {
|
||||
case 1:
|
||||
return { as: "h1", size: "xl", mt: 4, mb: 2 };
|
||||
return { as: 'h1', size: 'xl', mt: 4, mb: 2 };
|
||||
case 2:
|
||||
return { as: "h2", size: "lg", mt: 3, mb: 2 };
|
||||
return { as: 'h2', size: 'lg', mt: 3, mb: 2 };
|
||||
case 3:
|
||||
return { as: "h3", size: "md", mt: 2, mb: 1 };
|
||||
return { as: 'h3', size: 'md', mt: 2, mb: 1 };
|
||||
case 4:
|
||||
return { as: "h4", size: "sm", mt: 2, mb: 1 };
|
||||
return { as: 'h4', size: 'sm', mt: 2, mb: 1 };
|
||||
case 5:
|
||||
return { as: "h5", size: "sm", mt: 2, mb: 1 };
|
||||
return { as: 'h5', size: 'sm', mt: 2, mb: 1 };
|
||||
case 6:
|
||||
return { as: "h6", size: "xs", mt: 2, mb: 1 };
|
||||
return { as: 'h6', size: 'xs', mt: 2, mb: 1 };
|
||||
default:
|
||||
return { as: `h${depth}`, size: "md", mt: 2, mb: 1 };
|
||||
return { as: `h${depth}`, size: 'md', mt: 2, mb: 1 };
|
||||
}
|
||||
};
|
||||
|
||||
interface TableToken extends marked.Tokens.Table {
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
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 CustomHeading: React.FC<{ text: string; depth: number }> = ({ text, depth }) => {
|
||||
const headingProps = getHeadingProps(depth);
|
||||
return (
|
||||
<Heading {...headingProps} wordBreak="break-word" maxWidth="100%">
|
||||
@@ -83,9 +82,7 @@ const CustomHeading: React.FC<{ text: string; depth: number }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Text
|
||||
as="p"
|
||||
@@ -100,9 +97,7 @@ const CustomParagraph: React.FC<{ children: React.ReactNode }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Box
|
||||
as="blockquote"
|
||||
@@ -120,16 +115,9 @@ const CustomBlockquote: React.FC<{ children: React.ReactNode }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({
|
||||
code,
|
||||
language,
|
||||
}) => {
|
||||
const CustomCodeBlock: React.FC<{ code: string; language?: string }> = ({ code, language }) => {
|
||||
return (
|
||||
<MemoizedCodeBlock
|
||||
language={language}
|
||||
code={code}
|
||||
onRenderComplete={() => Promise.resolve()}
|
||||
/>
|
||||
<MemoizedCodeBlock language={language} code={code} onRenderComplete={() => Promise.resolve()} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -141,10 +129,10 @@ const CustomList: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ ordered, start, children }) => {
|
||||
const commonStyles = {
|
||||
fontSize: "sm",
|
||||
wordBreak: "break-word" as const,
|
||||
maxWidth: "100%" as const,
|
||||
stylePosition: "outside" as const,
|
||||
fontSize: 'sm',
|
||||
wordBreak: 'break-word' as const,
|
||||
maxWidth: '100%' as const,
|
||||
stylePosition: 'outside' as const,
|
||||
mb: 2,
|
||||
pl: 4,
|
||||
};
|
||||
@@ -166,16 +154,13 @@ const CustomListItem: React.FC<{
|
||||
return <ListItem mb={1}>{children}</ListItem>;
|
||||
};
|
||||
|
||||
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
|
||||
math,
|
||||
displayMode,
|
||||
}) => {
|
||||
const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({ math, displayMode }) => {
|
||||
const renderedMath = katex.renderToString(math, { displayMode });
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
display={displayMode ? 'block' : 'inline'}
|
||||
p={displayMode ? 4 : 1}
|
||||
my={displayMode ? 4 : 0}
|
||||
borderRadius="md"
|
||||
@@ -188,23 +173,17 @@ const CustomKatex: React.FC<{ math: string; displayMode: boolean }> = ({
|
||||
|
||||
const CustomTable: React.FC<{
|
||||
header: React.ReactNode[];
|
||||
align: Array<"center" | "left" | "right" | null>;
|
||||
align: Array<'center' | 'left' | 'right' | null>;
|
||||
rows: React.ReactNode[][];
|
||||
}> = ({ header, align, rows }) => {
|
||||
return (
|
||||
<Table
|
||||
variant="simple"
|
||||
size="sm"
|
||||
my={4}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
>
|
||||
<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"}
|
||||
textAlign={align[i] || 'left'}
|
||||
fontWeight="bold"
|
||||
p={2}
|
||||
minW={16}
|
||||
@@ -219,12 +198,7 @@ const CustomTable: React.FC<{
|
||||
{rows.map((row, rIndex) => (
|
||||
<Tr key={rIndex}>
|
||||
{row.map((cell, cIndex) => (
|
||||
<Td
|
||||
key={cIndex}
|
||||
textAlign={align[cIndex] || "left"}
|
||||
p={2}
|
||||
wordBreak="break-word"
|
||||
>
|
||||
<Td key={cIndex} textAlign={align[cIndex] || 'left'} p={2} wordBreak="break-word">
|
||||
{cell}
|
||||
</Td>
|
||||
))}
|
||||
@@ -241,13 +215,7 @@ const CustomHtmlBlock: React.FC<{ content: string }> = ({ content }) => {
|
||||
|
||||
const CustomText: React.FC<{ text: React.ReactNode }> = ({ text }) => {
|
||||
return (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
maxWidth="100%"
|
||||
as="span"
|
||||
>
|
||||
<Text fontSize="sm" lineHeight="short" wordBreak="break-word" maxWidth="100%" as="span">
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
@@ -262,13 +230,7 @@ const CustomStrong: React.FC<CustomStrongProps> = ({ children }) => {
|
||||
|
||||
const CustomEm: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<Text
|
||||
as="em"
|
||||
fontStyle="italic"
|
||||
lineHeight="short"
|
||||
wordBreak="break-word"
|
||||
display="inline"
|
||||
>
|
||||
<Text as="em" fontStyle="italic" lineHeight="short" wordBreak="break-word" display="inline">
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
@@ -289,7 +251,7 @@ const CustomDel: React.FC<{ text: string }> = ({ text }) => {
|
||||
};
|
||||
|
||||
const CustomCodeSpan: React.FC<{ code: string }> = ({ code }) => {
|
||||
const bg = useColorModeValue("gray.100", "gray.800");
|
||||
const bg = useColorModeValue('gray.100', 'gray.800');
|
||||
return (
|
||||
<Code
|
||||
fontSize="sm"
|
||||
@@ -312,13 +274,13 @@ const CustomMath: React.FC<{ math: string; displayMode?: boolean }> = ({
|
||||
return (
|
||||
<Box
|
||||
as="span"
|
||||
display={displayMode ? "block" : "inline"}
|
||||
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"}`}
|
||||
className={`math ${displayMode ? 'math-display' : 'math-inline'}`}
|
||||
>
|
||||
{math}
|
||||
</Box>
|
||||
@@ -336,8 +298,8 @@ const CustomLink: React.FC<{
|
||||
title={title}
|
||||
isExternal
|
||||
sx={{
|
||||
"& span": {
|
||||
color: "text.link",
|
||||
'& span': {
|
||||
color: 'text.link',
|
||||
},
|
||||
}}
|
||||
maxWidth="100%"
|
||||
@@ -379,46 +341,34 @@ function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
|
||||
tokens.forEach((token, i) => {
|
||||
switch (token.type) {
|
||||
case "heading":
|
||||
output.push(
|
||||
<CustomHeading key={i} text={token.text} depth={token.depth} />,
|
||||
);
|
||||
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;
|
||||
case 'paragraph': {
|
||||
const parsedContent = token.tokens ? parseTokens(token.tokens) : token.text;
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
blockquoteContent.push(<CustomParagraph key={i}>{parsedContent}</CustomParagraph>);
|
||||
} else {
|
||||
output.push(
|
||||
<CustomParagraph key={i}>{parsedContent}</CustomParagraph>,
|
||||
);
|
||||
output.push(<CustomParagraph key={i}>{parsedContent}</CustomParagraph>);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "br":
|
||||
case 'br':
|
||||
output.push(<br key={i} />);
|
||||
break;
|
||||
case "escape": {
|
||||
case 'escape': {
|
||||
break;
|
||||
}
|
||||
case "blockquote_start":
|
||||
case 'blockquote_start':
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
|
||||
case "blockquote_end":
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{parseTokens(blockquoteContent)}
|
||||
</CustomBlockquote>,
|
||||
);
|
||||
case 'blockquote_end':
|
||||
output.push(<CustomBlockquote key={i}>{parseTokens(blockquoteContent)}</CustomBlockquote>);
|
||||
blockquoteContent = [];
|
||||
break;
|
||||
case "blockquote": {
|
||||
case 'blockquote': {
|
||||
output.push(
|
||||
<CustomBlockquote key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : null}
|
||||
@@ -426,44 +376,30 @@ function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "math":
|
||||
output.push(
|
||||
<CustomMath key={i} math={(token as any).value} displayMode={true} />,
|
||||
);
|
||||
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}
|
||||
/>,
|
||||
);
|
||||
case 'inlineMath':
|
||||
output.push(<CustomMath key={i} math={(token as any).value} displayMode={false} />);
|
||||
break;
|
||||
case "inlineKatex":
|
||||
case "blockKatex": {
|
||||
case 'inlineKatex':
|
||||
case 'blockKatex': {
|
||||
const katexToken = token as any;
|
||||
output.push(
|
||||
<CustomKatex
|
||||
key={i}
|
||||
math={katexToken.text}
|
||||
displayMode={katexToken.displayMode}
|
||||
/>,
|
||||
<CustomKatex key={i} math={katexToken.text} displayMode={katexToken.displayMode} />,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "code":
|
||||
output.push(
|
||||
<CustomCodeBlock key={i} code={token.text} language={token.lang} />,
|
||||
);
|
||||
case 'code':
|
||||
output.push(<CustomCodeBlock key={i} code={token.text} language={token.lang} />);
|
||||
break;
|
||||
|
||||
case "hr":
|
||||
case 'hr':
|
||||
output.push(<CustomHr key={i} />);
|
||||
break;
|
||||
|
||||
case "list": {
|
||||
case 'list': {
|
||||
const { ordered, start, items } = token;
|
||||
const listItems = items.map((listItem, idx) => {
|
||||
const nestedContent = parseTokens(listItem.tokens);
|
||||
@@ -477,53 +413,43 @@ function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "table": {
|
||||
case 'table': {
|
||||
const tableToken = token as TableToken;
|
||||
|
||||
output.push(
|
||||
<CustomTable
|
||||
key={i}
|
||||
header={tableToken.header.map((cell) =>
|
||||
typeof cell === "string" ? cell : parseTokens(cell.tokens || []),
|
||||
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 || []),
|
||||
),
|
||||
rows={tableToken.rows.map(row =>
|
||||
row.map(cell => (typeof cell === 'string' ? cell : parseTokens(cell.tokens || []))),
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "html":
|
||||
case 'html':
|
||||
output.push(<CustomHtmlBlock key={i} content={token.text} />);
|
||||
break;
|
||||
case "def":
|
||||
case "space":
|
||||
case 'def':
|
||||
case 'space':
|
||||
break;
|
||||
case "strong":
|
||||
output.push(
|
||||
<CustomStrong key={i}>
|
||||
{parseTokens(token.tokens || [])}
|
||||
</CustomStrong>,
|
||||
);
|
||||
case 'strong':
|
||||
output.push(<CustomStrong key={i}>{parseTokens(token.tokens || [])}</CustomStrong>);
|
||||
break;
|
||||
case "em":
|
||||
case 'em':
|
||||
output.push(
|
||||
<CustomEm key={i}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
</CustomEm>,
|
||||
<CustomEm key={i}>{token.tokens ? parseTokens(token.tokens) : token.text}</CustomEm>,
|
||||
);
|
||||
break;
|
||||
|
||||
case "codespan":
|
||||
case 'codespan':
|
||||
output.push(<CustomCodeSpan key={i} code={token.text} />);
|
||||
break;
|
||||
|
||||
case "link":
|
||||
case 'link':
|
||||
output.push(
|
||||
<CustomLink key={i} href={token.href} title={token.title}>
|
||||
{token.tokens ? parseTokens(token.tokens) : token.text}
|
||||
@@ -531,33 +457,24 @@ function parseTokens(tokens: marked.Token[]): JSX.Element[] {
|
||||
);
|
||||
break;
|
||||
|
||||
case "image":
|
||||
case 'image':
|
||||
output.push(
|
||||
<CustomImage
|
||||
key={i}
|
||||
href={token.href}
|
||||
title={token.title}
|
||||
text={token.text}
|
||||
/>,
|
||||
<CustomImage key={i} href={token.href} title={token.title} text={token.text} />,
|
||||
);
|
||||
break;
|
||||
|
||||
case "text": {
|
||||
const parsedContent = token.tokens
|
||||
? parseTokens(token.tokens)
|
||||
: token.text;
|
||||
case 'text': {
|
||||
const parsedContent = token.tokens ? parseTokens(token.tokens) : token.text;
|
||||
|
||||
if (blockquoteContent.length > 0) {
|
||||
blockquoteContent.push(
|
||||
<React.Fragment key={i}>{parsedContent}</React.Fragment>,
|
||||
);
|
||||
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);
|
||||
console.warn('Unhandled token type:', token.type, token);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import React from "react";
|
||||
import {renderMessageMarkdown} from "./MessageMarkdown";
|
||||
import React from 'react';
|
||||
|
||||
import { renderMessageMarkdown } from './MessageMarkdown';
|
||||
|
||||
interface CustomMarkdownRendererProps {
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
const MessageMarkdownRenderer: React.FC<CustomMarkdownRendererProps> = ({
|
||||
markdown,
|
||||
}) => {
|
||||
const MessageMarkdownRenderer: React.FC<CustomMarkdownRendererProps> = ({ markdown }) => {
|
||||
return <div>{renderMessageMarkdown(markdown)}</div>;
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import {motion} from "framer-motion";
|
||||
import {Box} from "@chakra-ui/react";
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default motion(Box);
|
||||
export default motion(Box);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import { Edit2Icon } from "lucide-react";
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { Edit2Icon } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const UserMessageTools = observer(({ disabled = false, message, onEdit }) => (
|
||||
<IconButton
|
||||
@@ -8,26 +8,26 @@ const UserMessageTools = observer(({ disabled = false, message, onEdit }) => (
|
||||
color="text.primary"
|
||||
aria-label="Edit message"
|
||||
title="Edit message"
|
||||
icon={<Edit2Icon size={"1em"} />}
|
||||
icon={<Edit2Icon size={'1em'} />}
|
||||
onClick={() => onEdit(message)}
|
||||
_active={{
|
||||
bg: "transparent",
|
||||
bg: 'transparent',
|
||||
svg: {
|
||||
stroke: "brand.100",
|
||||
transition: "stroke 0.3s ease-in-out",
|
||||
stroke: 'brand.100',
|
||||
transition: 'stroke 0.3s ease-in-out',
|
||||
},
|
||||
}}
|
||||
_hover={{
|
||||
bg: "transparent",
|
||||
bg: 'transparent',
|
||||
svg: {
|
||||
stroke: "accent.secondary",
|
||||
transition: "stroke 0.3s ease-in-out",
|
||||
stroke: 'accent.secondary',
|
||||
transition: 'stroke 0.3s ease-in-out',
|
||||
},
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
_focus={{ boxShadow: "none" }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import messageEditorStore from '../../../../stores/MessageEditorStore';
|
||||
import MessageBubble from '../MessageBubble';
|
||||
import messageEditorStore from "../../../../stores/MessageEditorStore";
|
||||
|
||||
// Mock browser APIs
|
||||
class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
// Add ResizeObserver to the global object
|
||||
@@ -16,140 +17,140 @@ global.ResizeObserver = MockResizeObserver;
|
||||
|
||||
// Mock the Message model
|
||||
vi.mock('../../../../models/Message', () => ({
|
||||
default: {
|
||||
// This is needed for the Instance<typeof Message> type
|
||||
}
|
||||
default: {
|
||||
// This is needed for the Instance<typeof Message> type
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the stores
|
||||
vi.mock('../../../../stores/ClientChatStore', () => ({
|
||||
default: {
|
||||
items: [],
|
||||
isLoading: false,
|
||||
editMessage: vi.fn().mockReturnValue(true)
|
||||
}
|
||||
default: {
|
||||
items: [],
|
||||
isLoading: false,
|
||||
editMessage: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores/UserOptionsStore', () => ({
|
||||
default: {
|
||||
followModeEnabled: false,
|
||||
setFollowModeEnabled: vi.fn()
|
||||
}
|
||||
default: {
|
||||
followModeEnabled: false,
|
||||
setFollowModeEnabled: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the MessageEditorStore
|
||||
vi.mock('../../../../stores/MessageEditorStore', () => ({
|
||||
default: {
|
||||
editedContent: 'Test message',
|
||||
setEditedContent: vi.fn(),
|
||||
setMessage: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
handleSave: vi.fn().mockImplementation(function() {
|
||||
// Use the mocked messageEditorStore from the import
|
||||
messageEditorStore.onCancel();
|
||||
return Promise.resolve();
|
||||
})
|
||||
}
|
||||
default: {
|
||||
editedContent: 'Test message',
|
||||
setEditedContent: vi.fn(),
|
||||
setMessage: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
handleSave: vi.fn().mockImplementation(function () {
|
||||
// Use the mocked messageEditorStore from the import
|
||||
messageEditorStore.onCancel();
|
||||
return Promise.resolve();
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the MessageRenderer component
|
||||
vi.mock('../ChatMessageContent', () => ({
|
||||
default: ({ content }) => <div data-testid="message-content">{content}</div>
|
||||
default: ({ content }) => <div data-testid="message-content">{content}</div>,
|
||||
}));
|
||||
|
||||
// Mock the UserMessageTools component
|
||||
vi.mock('../UserMessageTools', () => ({
|
||||
default: ({ message, onEdit }) => (
|
||||
<button data-testid="edit-button" onClick={() => onEdit(message)}>
|
||||
Edit
|
||||
</button>
|
||||
)
|
||||
default: ({ message, onEdit }) => (
|
||||
<button data-testid="edit-button" onClick={() => onEdit(message)}>
|
||||
Edit
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../MotionBox", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
vi.mock('../MotionBox', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
|
||||
return { default: {
|
||||
...actual.default,
|
||||
div: (props: any) => React.createElement('div', props, props.children),
|
||||
motion: (props: any) => React.createElement('div', props, props.children),
|
||||
|
||||
}
|
||||
}
|
||||
return {
|
||||
default: {
|
||||
...actual.default,
|
||||
div: (props: any) => React.createElement('div', props, props.children),
|
||||
motion: (props: any) => React.createElement('div', props, props.children),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('MessageBubble', () => {
|
||||
const mockScrollRef = { current: { scrollTo: vi.fn() } };
|
||||
const mockUserMessage = {
|
||||
role: 'user',
|
||||
content: 'Test message'
|
||||
};
|
||||
const mockAssistantMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Assistant response'
|
||||
};
|
||||
const mockScrollRef = { current: { scrollTo: vi.fn() } };
|
||||
const mockUserMessage = {
|
||||
role: 'user',
|
||||
content: 'Test message',
|
||||
};
|
||||
const mockAssistantMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Assistant response',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render user message correctly', () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
expect(screen.getByText('You')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render assistant message correctly', () => {
|
||||
render(<MessageBubble msg={mockAssistantMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
expect(screen.getByText("Geoff's AI")).toBeInTheDocument();
|
||||
expect(screen.getByTestId('message-content')).toHaveTextContent('Assistant response');
|
||||
});
|
||||
|
||||
it('should show edit button on hover for user messages', async () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Simulate hover
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
|
||||
expect(screen.getByTestId('edit-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show editor when edit button is clicked', () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Simulate hover and click edit
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
fireEvent.click(screen.getByTestId('edit-button'));
|
||||
|
||||
// Check if the textarea is rendered (part of MessageEditor)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide editor after message is edited and saved', async () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Show the editor
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
fireEvent.click(screen.getByTestId('edit-button'));
|
||||
|
||||
// Verify editor is shown
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
// Find and click the save button
|
||||
const saveButton = screen.getByLabelText('Save edit');
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
// Wait for the editor to disappear
|
||||
await waitFor(() => {
|
||||
// Check that the editor is no longer visible
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
// And the message content is visible again
|
||||
expect(screen.getByText('Test message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render user message correctly', () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
expect(screen.getByText('You')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render assistant message correctly', () => {
|
||||
render(<MessageBubble msg={mockAssistantMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
expect(screen.getByText("Geoff's AI")).toBeInTheDocument();
|
||||
expect(screen.getByTestId('message-content')).toHaveTextContent('Assistant response');
|
||||
});
|
||||
|
||||
it('should show edit button on hover for user messages', async () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Simulate hover
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
|
||||
expect(screen.getByTestId('edit-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show editor when edit button is clicked', () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Simulate hover and click edit
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
fireEvent.click(screen.getByTestId('edit-button'));
|
||||
|
||||
// Check if the textarea is rendered (part of MessageEditor)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide editor after message is edited and saved', async () => {
|
||||
render(<MessageBubble msg={mockUserMessage} scrollRef={mockScrollRef} />);
|
||||
|
||||
// Show the editor
|
||||
fireEvent.mouseEnter(screen.getByRole('listitem'));
|
||||
fireEvent.click(screen.getByTestId('edit-button'));
|
||||
|
||||
// Verify editor is shown
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
// Find and click the save button
|
||||
const saveButton = screen.getByLabelText('Save edit');
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
// Wait for the editor to disappear
|
||||
await waitFor(() => {
|
||||
// Check that the editor is no longer visible
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
// And the message content is visible again
|
||||
expect(screen.getByText('Test message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify that handleSave was called
|
||||
expect(messageEditorStore.handleSave).toHaveBeenCalled();
|
||||
});
|
||||
// Verify that handleSave was called
|
||||
expect(messageEditorStore.handleSave).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import MessageEditor from '../MessageEditorComponent';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Import the mocked stores
|
||||
import clientChatStore from '../../../../stores/ClientChatStore';
|
||||
import messageEditorStore from '../../../../stores/MessageEditorStore';
|
||||
import MessageEditor from '../MessageEditorComponent';
|
||||
|
||||
// Mock the Message model
|
||||
vi.mock('../../../../models/Message', () => {
|
||||
return {
|
||||
default: {
|
||||
// This is needed for the Instance<typeof Message> type
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock fetch globally
|
||||
globalThis.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({})
|
||||
})
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Mock the ClientChatStore
|
||||
@@ -31,14 +31,14 @@ vi.mock('../../../../stores/ClientChatStore', () => {
|
||||
removeAfter: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
setIsLoading: vi.fn(),
|
||||
editMessage: vi.fn().mockReturnValue(true)
|
||||
editMessage: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
// Add the mockUserMessage to the items array
|
||||
mockStore.items.indexOf = vi.fn().mockReturnValue(0);
|
||||
|
||||
return {
|
||||
default: mockStore
|
||||
default: mockStore,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -48,25 +48,25 @@ vi.mock('../../../../stores/MessageEditorStore', () => {
|
||||
editedContent: 'Test message', // Set initial value to match the test expectation
|
||||
message: null,
|
||||
setEditedContent: vi.fn(),
|
||||
setMessage: vi.fn((message) => {
|
||||
setMessage: vi.fn(message => {
|
||||
mockStore.message = message;
|
||||
mockStore.editedContent = message.content;
|
||||
}),
|
||||
onCancel: vi.fn(),
|
||||
handleSave: vi.fn()
|
||||
handleSave: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
default: mockStore
|
||||
default: mockStore,
|
||||
};
|
||||
});
|
||||
|
||||
describe('MessageEditor', () => {
|
||||
// Create a message object with a setContent method
|
||||
const mockUserMessage = {
|
||||
content: 'Test message',
|
||||
const mockUserMessage = {
|
||||
content: 'Test message',
|
||||
role: 'user',
|
||||
setContent: vi.fn()
|
||||
setContent: vi.fn(),
|
||||
};
|
||||
const mockOnCancel = vi.fn();
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('MessageEditor', () => {
|
||||
});
|
||||
|
||||
it('should call handleSave when save button is clicked', () => {
|
||||
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel}/>);
|
||||
render(<MessageEditor message={mockUserMessage} onCancel={mockOnCancel} />);
|
||||
|
||||
const saveButton = screen.getByLabelText('Save edit');
|
||||
fireEvent.click(saveButton);
|
||||
|
Reference in New Issue
Block a user