Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/client/packages/design-system/widgets/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const esModules = ["remark-gfm"].join("|");

module.exports = {
preset: "ts-jest",
roots: ["<rootDir>/src"],
Expand All @@ -6,6 +8,9 @@ module.exports = {
moduleNameMapper: {
"\\.(css)$": "<rootDir>../../../test/__mocks__/styleMock.js",
},
transformIgnorePatterns: [
`[/\\\\]node_modules[/\\\\](?!${esModules}).+\\.(js|jsx|mjs|cjs|ts|tsx)$`,
],
globals: {
"ts-jest": {
useESM: true,
Expand Down
3 changes: 2 additions & 1 deletion app/client/packages/design-system/widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"lodash": "*",
"react-aria-components": "^1.2.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0"
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to handle the table in markdown.

},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Button, Flex, Text, TextArea } from "@appsmith/wds";
import type { FormEvent, ForwardedRef, KeyboardEvent } from "react";
import React, { forwardRef, useCallback } from "react";
import { ChatDescriptionModal } from "./ChatDescriptionModal";
import { ChatTitle } from "./ChatTitle";
import styles from "./styles.module.css";
import { ThreadMessage } from "./ThreadMessage";
import type { AIChatProps, ChatMessage } from "./types";
import { UserAvatar } from "./UserAvatar";
import type { ForwardedRef } from "react";
import React, { forwardRef } from "react";

const MIN_PROMPT_LENGTH = 3;
import styles from "./styles.module.css";
import { ChatHeader } from "./ChatHeader";
import { ChatThread } from "./ChatThread";
import type { AIChatProps } from "./types";
import { ChatInputSection } from "./ChatInputSection";

const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
Expand All @@ -25,81 +22,28 @@ const _AIChat = (props: AIChatProps, ref: ForwardedRef<HTMLDivElement>) => {
username,
...rest
} = props;
const [isChatDescriptionModalOpen, setIsChatDescriptionModalOpen] =
React.useState(false);

const handleFormSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit?.();
},
[onSubmit],
);

const handlePromptInputKeyDown = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
onSubmit?.();
}
},
[onSubmit],
);

return (
<div className={styles.root} ref={ref} {...rest}>
<ChatDescriptionModal
isOpen={isChatDescriptionModalOpen}
setOpen={() =>
setIsChatDescriptionModalOpen(!isChatDescriptionModalOpen)
}
>
{chatDescription}
</ChatDescriptionModal>

<div className={styles.header}>
<Flex alignItems="center" gap="spacing-2">
<ChatTitle title={chatTitle} />
<Button
icon="info-square-rounded"
onPress={() => setIsChatDescriptionModalOpen(true)}
variant="ghost"
/>
</Flex>

<Flex alignItems="center" gap="spacing-2">
<UserAvatar username={username} />
<Text data-testid="t--aichat-username" size="body">
{username}
</Text>
</Flex>
</div>

<ul className={styles.thread} data-testid="t--aichat-thread">
{thread.map((message: ChatMessage) => (
<ThreadMessage
{...message}
key={message.id}
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
username={username}
/>
))}
</ul>

<form className={styles.promptForm} onSubmit={handleFormSubmit}>
<TextArea
// TODO: Handle isWaitingForResponse: true state
isDisabled={isWaitingForResponse}
name="prompt"
onChange={onPromptChange}
onKeyDown={handlePromptInputKeyDown}
placeholder={promptInputPlaceholder}
value={prompt}
/>
<Button isDisabled={prompt.length < MIN_PROMPT_LENGTH} type="submit">
Send
</Button>
</form>
<ChatHeader
chatDescription={chatDescription}
chatTitle={chatTitle}
username={username}
/>

<ChatThread
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
thread={thread}
username={username}
/>

<ChatInputSection
isWaitingForResponse={isWaitingForResponse}
onPromptChange={onPromptChange}
onSubmit={onSubmit}
prompt={prompt}
promptInputPlaceholder={promptInputPlaceholder}
/>
</div>
);
};
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useState } from "react";
import {
Avatar,
Button,
Flex,
Modal,
ModalBody,
ModalContent,
ModalHeader,
Text,
} from "@appsmith/wds";

import styles from "./styles.module.css";

// this value might come from props in future. So keeping a temporary value here.
const LOGO =
"https://app.appsmith.com/static/media/appsmith_logo_square.3867b1959653dabff8dc.png";
Comment on lines +1 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Class, let's discuss the imports and constants!

Good job on organizing your imports, but I have a small suggestion for you. The LOGO constant on lines 16-17 seems a bit out of place here. Remember, we always want to keep our code tidy and organized!

Consider moving the LOGO constant to a separate file, perhaps named constants.ts. This way, we keep our component file focused on its main job and make it easier to manage these global values. Here's how you might do it:

  1. Create a new file constants.ts:
export const LOGO = "https://app.appsmith.com/static/media/appsmith_logo_square.3867b1959653dabff8dc.png";
  1. Then in this file, you can import it like so:
import { LOGO } from './constants';

Remember, good organization is key to writing maintainable code!


export const ChatHeader: React.FC<{
chatTitle?: string;
username: string;
chatDescription?: string;
}> = ({ chatDescription, chatTitle, username }) => {
const [isChatDescriptionModalOpen, setIsChatDescriptionModalOpen] =
useState(false);

return (
<>
<div className={styles.header}>
<Flex alignItems="center" gap="spacing-2">
<Flex alignItems="center" gap="spacing-3">
<Avatar label="Appsmith AI" size="large" src={LOGO} />
<Text fontWeight={600} size="subtitle">
{chatTitle}
</Text>
</Flex>
<Button
icon="info-square-rounded"
onPress={() => setIsChatDescriptionModalOpen(true)}
variant="ghost"
/>
</Flex>
<Flex alignItems="center" gap="spacing-2">
<Avatar label={username} />
<Text data-testid="t--aichat-username" size="body">
{username}
</Text>
</Flex>
</div>

<Modal
isOpen={isChatDescriptionModalOpen}
setOpen={setIsChatDescriptionModalOpen}
>
<ModalContent>
<ModalHeader title="Information about the bot" />
<ModalBody>{chatDescription}</ModalBody>
</ModalContent>
</Modal>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";
import { Flex, ChatInput, Icon, Text } from "@appsmith/wds";

const MIN_PROMPT_LENGTH = 3;

export const ChatInputSection: React.FC<{
isWaitingForResponse: boolean;
prompt: string;
promptInputPlaceholder?: string;
onPromptChange: (value: string) => void;
onSubmit?: () => void;
}> = ({
isWaitingForResponse,
onPromptChange,
onSubmit,
prompt,
promptInputPlaceholder,
}) => (
<Flex
direction="column"
gap="spacing-3"
paddingBottom="spacing-4"
paddingLeft="spacing-6"
paddingRight="spacing-6"
paddingTop="spacing-4"
>
<ChatInput
isLoading={isWaitingForResponse}
isSubmitDisabled={prompt.length < MIN_PROMPT_LENGTH}
onChange={onPromptChange}
onSubmit={onSubmit}
placeholder={promptInputPlaceholder}
value={prompt}
/>
<Flex
alignItems="center"
flexGrow={1}
gap="spacing-1"
justifyContent="center"
>
<Icon name="alert-circle" size="small" />
<Text color="neutral" size="caption" textAlign="center">
LLM assistant can make mistakes. Answers should be verified before they
are trusted.
</Text>
</Flex>
</Flex>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import { Avatar, Flex, Markdown } from "@appsmith/wds";

import styles from "./styles.module.css";
import type { ChatMessage } from "./types";
import { AssistantSuggestionButton } from "./AssistantSuggestionButton";

export const ChatThread: React.FC<{
thread: ChatMessage[];
onApplyAssistantSuggestion?: (suggestion: string) => void;
username: string;
}> = ({ onApplyAssistantSuggestion, thread, username }) => (
<Flex direction="column" gap="spacing-3" padding="spacing-6">
{thread.map((message: ChatMessage) => {
const { content, isAssistant, promptSuggestions = [] } = message;

return (
<Flex direction={isAssistant ? "row" : "row-reverse"} key={message.id}>
{isAssistant && (
<div>
<Markdown>{content}</Markdown>

{promptSuggestions.length > 0 && (
<Flex
className={styles.suggestions}
gap="spacing-5"
paddingTop="spacing-4"
wrap="wrap"
>
{promptSuggestions.map((suggestion) => (
<AssistantSuggestionButton
key={suggestion}
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
onPress={() => onApplyAssistantSuggestion?.(suggestion)}
>
{suggestion}
</AssistantSuggestionButton>
))}
Comment on lines +30 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Let's optimize your suggestion buttons, students!

Your implementation of the AssistantSuggestionButton is quite good, like a well-written essay. However, there's room for improvement in how we handle the onPress event.

Remember our lesson on callback functions? Using inline functions can cause unnecessary re-renders, like rewriting your entire essay when you only need to change a single word!

Let's optimize this by using the useCallback hook:

import React, { useCallback } from 'react';

// ... in your component
const handleSuggestionPress = useCallback((suggestion: string) => {
  onApplyAssistantSuggestion?.(suggestion);
}, [onApplyAssistantSuggestion]);

// ... in your JSX
<AssistantSuggestionButton
  key={suggestion}
  onPress={() => handleSuggestionPress(suggestion)}
>
  {suggestion}
</AssistantSuggestionButton>

This way, we're creating a stable callback function, like using a template for your essays - efficient and consistent!

</Flex>
)}
</div>
)}
{!isAssistant && (
<Flex direction="row-reverse" gap="spacing-3">
<Avatar label={username} />
<div>{content}</div>
</Flex>
)}
</Flex>
);
})}
</Flex>
);

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading