chore: Update markdown component + create avatar component + refactor (#36832)
/ok-to-test tags="@tag.Anvil" <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced the `Avatar` component for displaying user avatars. - Added the `Markdown` component for rendering Markdown content with GitHub Flavored Markdown support. - Enhanced `AIChat` component with a new modular input system and improved message display. - **Bug Fixes** - Updated CSS for better styling consistency across components. - **Refactor** - Removed outdated components (`ChatDescriptionModal` and `ThreadMessage`) to streamline the codebase. - Simplified the structure of `AIChat` by incorporating new components. - **Documentation** - Updated Storybook stories for `AIChat`, `Avatar`, and `Markdown` components to showcase new features. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/11320929997> > Commit: db2bc5c9a0a0a65b3d9dd4f53e9f100beb3af3a7 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11320929997&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Anvil` > Spec: > <hr>Mon, 14 Oct 2024 04:42:54 UTC <!-- end of auto-generated comment: Cypress test results -->
This commit is contained in:
parent
ef5a253a92
commit
a7bf302f9a
|
|
@ -1,3 +1,5 @@
|
|||
const esModules = ["remark-gfm"].join("|");
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
roots: ["<rootDir>/src"],
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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>
|
||||
<ChatHeader
|
||||
chatDescription={chatDescription}
|
||||
chatTitle={chatTitle}
|
||||
username={username}
|
||||
/>
|
||||
|
||||
<div className={styles.header}>
|
||||
<Flex alignItems="center" gap="spacing-2">
|
||||
<ChatTitle title={chatTitle} />
|
||||
<Button
|
||||
icon="info-square-rounded"
|
||||
onPress={() => setIsChatDescriptionModalOpen(true)}
|
||||
variant="ghost"
|
||||
/>
|
||||
</Flex>
|
||||
<ChatThread
|
||||
onApplyAssistantSuggestion={onApplyAssistantSuggestion}
|
||||
thread={thread}
|
||||
username={username}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<ChatInputSection
|
||||
isWaitingForResponse={isWaitingForResponse}
|
||||
onPromptChange={onPromptChange}
|
||||
onSubmit={onSubmit}
|
||||
prompt={prompt}
|
||||
promptInputPlaceholder={promptInputPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { Modal, ModalBody, ModalContent, ModalHeader } from "@appsmith/wds";
|
||||
import React from "react";
|
||||
import type { ChatDescriptionModalProps } from "./types";
|
||||
|
||||
export const ChatDescriptionModal = ({
|
||||
children,
|
||||
...rest
|
||||
}: ChatDescriptionModalProps) => {
|
||||
return (
|
||||
<Modal {...rest}>
|
||||
<ModalContent>
|
||||
<ModalHeader title="Information about the bot" />
|
||||
<ModalBody>{children}</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ChatDescriptionModal";
|
||||
export * from "./types";
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import type { ModalProps } from "@appsmith/wds";
|
||||
|
||||
export interface ChatDescriptionModalProps extends ModalProps {}
|
||||
|
|
@ -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";
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isAssistant && (
|
||||
<Flex direction="row-reverse" gap="spacing-3">
|
||||
<Avatar label={username} />
|
||||
<div>{content}</div>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { clsx } from "clsx";
|
||||
import React from "react";
|
||||
import styles from "./styles.module.css";
|
||||
import type { ChatTitleProps } from "./types";
|
||||
|
||||
export const ChatTitle = ({ className, title, ...rest }: ChatTitleProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.root, className)}
|
||||
{...rest}
|
||||
data-testid="t--aichat-chat-title"
|
||||
>
|
||||
<div className={styles.logo} />
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ChatTitle";
|
||||
export * from "./types";
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
.root {
|
||||
display: flex;
|
||||
gap: var(--inner-spacing-3);
|
||||
align-items: center;
|
||||
/* TODO: --type-title doesn't exists. Define it */
|
||||
font-size: var(--type-title, 22.499px);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
/* TODO: --type-title-lineheight doesn't exists. Define it */
|
||||
line-height: var(--type-title-lineheight, 31.7px);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--border-radius-elevation-2);
|
||||
/* TODO: --bd-neutral doesn't exists. Define it */
|
||||
border: 1px solid var(--bd-neutral, #81858b);
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import type { HTMLProps } from "react";
|
||||
|
||||
export interface ChatTitleProps extends HTMLProps<HTMLDivElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { Flex, Text } from "@appsmith/wds";
|
||||
import { clsx } from "clsx";
|
||||
import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { monokai } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||
import { AssistantSuggestionButton } from "../AssistantSuggestionButton";
|
||||
import { UserAvatar } from "../UserAvatar";
|
||||
import styles from "./styles.module.css";
|
||||
import type { ThreadMessageProps } from "./types";
|
||||
|
||||
export const ThreadMessage = ({
|
||||
className,
|
||||
content,
|
||||
isAssistant,
|
||||
onApplyAssistantSuggestion,
|
||||
promptSuggestions = [],
|
||||
username,
|
||||
...rest
|
||||
}: ThreadMessageProps) => {
|
||||
return (
|
||||
<li
|
||||
className={clsx(styles.root, className)}
|
||||
data-assistant={isAssistant}
|
||||
{...rest}
|
||||
>
|
||||
{isAssistant ? (
|
||||
<div>
|
||||
<Text className={styles.content}>
|
||||
<Markdown
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
components={{
|
||||
code(props) {
|
||||
const { children, className, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className ?? "");
|
||||
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
style={monokai}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
</Text>
|
||||
|
||||
{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>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<UserAvatar className={styles.userAvatar} username={username} />
|
||||
<div>
|
||||
<Text className={styles.content}>{content}</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ThreadMessage";
|
||||
export * from "./types";
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
.root {
|
||||
display: flex;
|
||||
gap: var(--inner-spacing-4);
|
||||
padding: var(--inner-spacing-3) 0;
|
||||
}
|
||||
|
||||
@container (min-width: 700px) {
|
||||
.root {
|
||||
padding: var(--inner-spacing-5) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.root[data-assistant="false"] {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.root[data-assistant="false"] .sentTime {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sentTime {
|
||||
margin: 0 0 var(--outer-spacing-3);
|
||||
/* TODO: --type-caption doesn't exists. Define it */
|
||||
font-size: var(--type-caption, 12.247px);
|
||||
/* TODO: --type-caption-lineheight doesn't exists. Define it */
|
||||
line-height: var(--type-caption-lineheight, 17.25px);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import type { HTMLProps } from "react";
|
||||
|
||||
export interface ThreadMessageProps extends HTMLProps<HTMLLIElement> {
|
||||
content: string;
|
||||
isAssistant: boolean;
|
||||
username: string;
|
||||
promptSuggestions?: string[];
|
||||
onApplyAssistantSuggestion?: (suggestion: string) => void;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { clsx } from "clsx";
|
||||
import React from "react";
|
||||
import styles from "./styles.module.css";
|
||||
import type { UserAvatarProps } from "./types";
|
||||
|
||||
export const UserAvatar = ({
|
||||
className,
|
||||
username,
|
||||
...rest
|
||||
}: UserAvatarProps) => {
|
||||
const getNameInitials = (username: string) => {
|
||||
const names = username.split(" ");
|
||||
|
||||
// If there is only one name, return the first character of the name.
|
||||
if (names.length === 1) {
|
||||
return `${names[0].charAt(0)}`;
|
||||
}
|
||||
|
||||
return `${names[0].charAt(0)}${names[1]?.charAt(0)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={clsx(styles.root, className)} {...rest}>
|
||||
{getNameInitials(username)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./UserAvatar";
|
||||
export * from "./types";
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.root {
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
color: var(--bg-elevation-2, #fff);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
border-radius: var(--inner-spacing-1, 4px);
|
||||
background: #000;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import type { HTMLProps } from "react";
|
||||
|
||||
export interface UserAvatarProps extends HTMLProps<HTMLSpanElement> {
|
||||
username: string;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
.root {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-elevation-1);
|
||||
border: 1px solid var(--color-bd-elevation-1);
|
||||
/* TODO: --bg-elevation-1 doesn't exists. Define it */
|
||||
background: var(--bg-elevation-1, #fbfcfd);
|
||||
background-color: var(--color-bg-elevation-3);
|
||||
border-radius: var(--border-radius-elevation-3);
|
||||
outline: var(--border-width-1) solid var(--color-bd-elevation-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -13,7 +13,6 @@
|
|||
padding: var(--inner-spacing-5) var(--inner-spacing-6);
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid var(--color-bd-elevation-1);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
@container (min-width: 700px) {
|
||||
|
|
@ -23,21 +22,3 @@
|
|||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--inner-spacing-3);
|
||||
align-self: stretch;
|
||||
padding: 0px var(--inner-spacing-6) var(--inner-spacing-5)
|
||||
var(--inner-spacing-6);
|
||||
}
|
||||
|
||||
.promptForm {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: var(--outer-spacing-4) 0 0 0;
|
||||
padding: 0 var(--inner-spacing-5) var(--inner-spacing-5)
|
||||
var(--inner-spacing-5);
|
||||
gap: var(--outer-spacing-3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import React from "react";
|
||||
import { AIChat } from "@appsmith/wds";
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta: Meta<typeof AIChat> = {
|
||||
|
|
@ -19,6 +21,12 @@ export const Main: Story = {
|
|||
assistantName: "",
|
||||
isWaitingForResponse: false,
|
||||
},
|
||||
render: (args) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [prompt, setPrompt] = useState(args.prompt);
|
||||
|
||||
return <AIChat {...args} onPromptChange={setPrompt} prompt={prompt} />;
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyHistory: Story = {
|
||||
|
|
@ -69,6 +77,21 @@ export const WithHistory: Story = {
|
|||
content: "Thank you",
|
||||
isAssistant: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
content: `Here's an example of markdown code:
|
||||
|
||||
\`\`\`javascript
|
||||
function greet(name) {
|
||||
console.log(\`Hello, \${name}!\`);
|
||||
}
|
||||
|
||||
greet('World');
|
||||
\`\`\`
|
||||
|
||||
This code defines a function that greets the given name.`,
|
||||
isAssistant: true,
|
||||
},
|
||||
],
|
||||
prompt: "",
|
||||
username: "John Doe",
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
import { AIChat, type AIChatProps } from "..";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
const renderComponent = (props: Partial<AIChatProps> = {}) => {
|
||||
const defaultProps: AIChatProps = {
|
||||
username: "",
|
||||
thread: [],
|
||||
prompt: "",
|
||||
onPromptChange: jest.fn(),
|
||||
};
|
||||
|
||||
return render(<AIChat {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
describe("@appsmith/wds/AIChat", () => {
|
||||
it("should render chat's title", () => {
|
||||
const chatTitle = faker.lorem.words(2);
|
||||
|
||||
renderComponent({ chatTitle });
|
||||
|
||||
expect(screen.getByTestId("t--aichat-chat-title")).toHaveTextContent(
|
||||
chatTitle,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render username", () => {
|
||||
const username = faker.name.firstName();
|
||||
|
||||
renderComponent({ username });
|
||||
|
||||
expect(screen.getByTestId("t--aichat-username")).toHaveTextContent(
|
||||
username,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render thread", () => {
|
||||
const thread = [
|
||||
{
|
||||
id: faker.datatype.uuid(),
|
||||
content: faker.lorem.paragraph(1),
|
||||
isAssistant: false,
|
||||
},
|
||||
{
|
||||
id: faker.datatype.uuid(),
|
||||
content: faker.lorem.paragraph(2),
|
||||
isAssistant: true,
|
||||
},
|
||||
];
|
||||
|
||||
renderComponent({ thread });
|
||||
|
||||
const messages = within(
|
||||
screen.getByTestId("t--aichat-thread"),
|
||||
).getAllByRole("listitem");
|
||||
|
||||
expect(messages).toHaveLength(thread.length);
|
||||
expect(messages[0]).toHaveTextContent(thread[0].content);
|
||||
expect(messages[1]).toHaveTextContent(thread[1].content);
|
||||
});
|
||||
|
||||
it("should render prompt input placeholder", () => {
|
||||
const promptInputPlaceholder = faker.lorem.words(3);
|
||||
|
||||
renderComponent({
|
||||
promptInputPlaceholder,
|
||||
});
|
||||
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute(
|
||||
"placeholder",
|
||||
promptInputPlaceholder,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render prompt input value", () => {
|
||||
const prompt = faker.lorem.words(3);
|
||||
|
||||
renderComponent({
|
||||
prompt,
|
||||
});
|
||||
|
||||
expect(screen.getByRole("textbox")).toHaveValue(prompt);
|
||||
});
|
||||
|
||||
it("should trigger user's prompt", async () => {
|
||||
const onPromptChange = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
onPromptChange,
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByRole("textbox"), "A");
|
||||
|
||||
expect(onPromptChange).toHaveBeenCalledWith("A");
|
||||
});
|
||||
|
||||
it("should submit user's prompt", async () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
prompt: "ABCD",
|
||||
onSubmit,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Send" }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React from "react";
|
||||
import { clsx } from "clsx";
|
||||
import { Text } from "@appsmith/wds";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
import type { AvatarProps } from "./types";
|
||||
import { getTypographyClassName } from "@appsmith/wds-theming";
|
||||
|
||||
export const Avatar = (props: AvatarProps) => {
|
||||
const { className, label, size, src, ...rest } = props;
|
||||
|
||||
const getLabelInitials = (label: string) => {
|
||||
const names = label.split(" ");
|
||||
|
||||
if (names.length === 1) {
|
||||
return `${names[0].charAt(0)}`;
|
||||
}
|
||||
|
||||
return `${names[0].charAt(0)}${names[1]?.charAt(0)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(styles.avatar, className)}
|
||||
{...rest}
|
||||
data-size={size ? size : undefined}
|
||||
>
|
||||
{Boolean(src) ? (
|
||||
<img alt={label} className={styles.avatarImage} src={src} />
|
||||
) : (
|
||||
<Text className={getTypographyClassName("body")} fontWeight={500}>
|
||||
{getLabelInitials(label)}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./types";
|
||||
export * from "./Avatar";
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
.avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--sizing-8);
|
||||
height: var(--sizing-8);
|
||||
}
|
||||
|
||||
.avatar[data-size="small"] {
|
||||
width: var(--sizing-6);
|
||||
height: var(--sizing-6);
|
||||
}
|
||||
|
||||
.avatar[data-size="large"] {
|
||||
width: var(--sizing-10);
|
||||
height: var(--sizing-10);
|
||||
}
|
||||
|
||||
/* if the avatar has div, that means no source is provided. For this case, we want to add a background color and border radius */
|
||||
.avatar:has(div) {
|
||||
background-color: var(--color-bg-assistive);
|
||||
color: var(--color-fg-on-assistive);
|
||||
border-radius: var(--border-radius-elevation-3);
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { HTMLProps } from "react";
|
||||
|
||||
export interface AvatarProps extends Omit<HTMLProps<HTMLSpanElement>, "size"> {
|
||||
/** The label of the avatar */
|
||||
label: string;
|
||||
/** The image source of the avatar */
|
||||
src?: string;
|
||||
/** The size of the avatar
|
||||
*
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import { Avatar, Flex } from "@appsmith/wds";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
title: "WDS/Widgets/Avatar",
|
||||
component: Avatar,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Avatar>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "John Doe",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithImage: Story = {
|
||||
args: {
|
||||
label: "Jane Smith",
|
||||
src: "https://assets.appsmith.com/integrations/25720743.png",
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleInitial: Story = {
|
||||
args: {
|
||||
label: "Alice",
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
args: {
|
||||
label: "Alice",
|
||||
},
|
||||
render: (args) => (
|
||||
<Flex gap="spacing-2">
|
||||
<Avatar {...args} size="small" />
|
||||
<Avatar {...args} size="medium" />
|
||||
<Avatar {...args} size="large" />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
|
@ -6,10 +6,10 @@ import {
|
|||
IconButton,
|
||||
TextAreaInput,
|
||||
} from "@appsmith/wds";
|
||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||
import { useControlledState } from "@react-stately/utils";
|
||||
import { chain, useLayoutEffect } from "@react-aria/utils";
|
||||
import { TextField as HeadlessTextField } from "react-aria-components";
|
||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||
|
||||
import type { ChatInputProps } from "./types";
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ export function ChatInput(props: ChatInputProps) {
|
|||
isLoading,
|
||||
isReadOnly,
|
||||
isRequired,
|
||||
isSubmitDisabled,
|
||||
label,
|
||||
onChange,
|
||||
onSubmit,
|
||||
|
|
@ -91,12 +92,14 @@ export function ChatInput(props: ChatInputProps) {
|
|||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (Boolean(isSubmitDisabled)) return;
|
||||
|
||||
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
onSubmit?.();
|
||||
}
|
||||
},
|
||||
[onSubmit],
|
||||
[onSubmit, isSubmitDisabled],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
|
@ -112,14 +115,18 @@ export function ChatInput(props: ChatInputProps) {
|
|||
return (
|
||||
<IconButton
|
||||
icon="player-stop-filled"
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={Boolean(isDisabled) || Boolean(isSubmitDisabled)}
|
||||
onPress={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton icon="arrow-up" isDisabled={isDisabled} onPress={onSubmit} />
|
||||
<IconButton
|
||||
icon="arrow-up"
|
||||
isDisabled={Boolean(isDisabled) || Boolean(isSubmitDisabled)}
|
||||
onPress={onSubmit}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import type { TextAreaProps } from "@appsmith/wds";
|
||||
|
||||
export interface ChatInputProps extends TextAreaProps {
|
||||
/** callback function when the user submits the chat input */
|
||||
onSubmit?: () => void;
|
||||
/** flag for disable the submit button */
|
||||
isSubmitDisabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,3 +66,9 @@ export const Validation: Story = {
|
|||
</Form>
|
||||
),
|
||||
};
|
||||
|
||||
export const SubmitDisabled: Story = {
|
||||
args: {
|
||||
isSubmitDisabled: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./Link";
|
||||
export { default as linkStyles } from "./styles.module.css";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const Main: Story = {
|
|||
args: {
|
||||
target: "_blank",
|
||||
href: "https://appsmith.com",
|
||||
children: "This is a link.",
|
||||
children: "Appsmith.",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from "./src";
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import { clsx } from "clsx";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { getTypographyClassName } from "@appsmith/wds-theming";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
import { components } from "./components";
|
||||
import type { MarkdownProps } from "./types";
|
||||
|
||||
export const Markdown = (props: MarkdownProps) => {
|
||||
const { children, className, options, ...rest } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.markdown,
|
||||
getTypographyClassName("body"),
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<ReactMarkdown
|
||||
components={components}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
{...options}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Components } from "react-markdown";
|
||||
|
||||
import { a } from "./mdComponents/Link";
|
||||
import { code } from "./mdComponents/Code";
|
||||
import { p } from "./mdComponents/Paragraph";
|
||||
import { ul, ol, li } from "./mdComponents/List";
|
||||
import { h1, h2, h3, h4, h5, h6 } from "./mdComponents/Heading";
|
||||
|
||||
export const components: Components = {
|
||||
a,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
code,
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./types";
|
||||
export * from "./Markdown";
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Button, Flex, Text } from "@appsmith/wds";
|
||||
import type { ExtraProps } from "react-markdown";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useThemeContext } from "@appsmith/wds-theming";
|
||||
import {
|
||||
atomOneDark as darkTheme,
|
||||
atomOneLight as lightTheme,
|
||||
} from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
|
||||
type CodeProps = React.ClassAttributes<HTMLElement> &
|
||||
React.HTMLAttributes<HTMLElement> &
|
||||
ExtraProps;
|
||||
|
||||
export const Code = (props: CodeProps) => {
|
||||
const { children, className, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className ?? "");
|
||||
const theme = useThemeContext();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(String(children)).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}, [children]);
|
||||
|
||||
return match ? (
|
||||
<div data-component="code">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
padding="spacing-1"
|
||||
>
|
||||
<Text size="caption">{match[1]}</Text>
|
||||
<Button icon="copy" onPress={handleCopy} size="small" variant="ghost">
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</Flex>
|
||||
<SyntaxHighlighter
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
backgroundColor: "var(--color-bg-neutral-subtle)",
|
||||
}}
|
||||
language={match[1]}
|
||||
style={theme.colorMode === "dark" ? darkTheme : lightTheme}
|
||||
useInlineStyles
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
export { Code as code };
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React from "react";
|
||||
import type { Ref } from "react";
|
||||
import type { ExtraProps } from "react-markdown";
|
||||
import { Text, type TextProps } from "@appsmith/wds";
|
||||
|
||||
type HeadingProps = React.ClassAttributes<HTMLDivElement> &
|
||||
React.HTMLAttributes<HTMLDivElement> &
|
||||
ExtraProps;
|
||||
|
||||
const createHeading = (
|
||||
size: TextProps["size"],
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
fontWeight: TextProps["fontWeight"] = 700,
|
||||
) => {
|
||||
const HeadingComponent = ({ children, ref }: HeadingProps) => (
|
||||
<Text
|
||||
color="neutral"
|
||||
data-component={`h${level}`}
|
||||
fontWeight={fontWeight}
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
size={size}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
HeadingComponent.displayName = `Heading${level}`;
|
||||
|
||||
return HeadingComponent;
|
||||
};
|
||||
|
||||
export const h1 = createHeading("heading", 1);
|
||||
export const h2 = createHeading("title", 2);
|
||||
export const h3 = createHeading("subtitle", 3);
|
||||
export const h4 = createHeading("body", 4);
|
||||
export const h5 = createHeading("body", 5, 500);
|
||||
export const h6 = createHeading("body", 6, 300);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { Link } from "@appsmith/wds";
|
||||
import type { ExtraProps } from "react-markdown";
|
||||
|
||||
type LinkProps = React.ClassAttributes<HTMLAnchorElement> &
|
||||
React.AnchorHTMLAttributes<HTMLAnchorElement> &
|
||||
ExtraProps;
|
||||
|
||||
export const a = (props: LinkProps) => {
|
||||
const { children, href } = props;
|
||||
|
||||
return (
|
||||
<Link data-component="a" href={href} rel="noreferrer" target="_blank">
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import type { ExtraProps } from "react-markdown";
|
||||
|
||||
type ULProps = React.ClassAttributes<HTMLUListElement> &
|
||||
React.HTMLAttributes<HTMLUListElement> &
|
||||
ExtraProps;
|
||||
|
||||
export const ul = (props: ULProps) => {
|
||||
const { children } = props;
|
||||
|
||||
return <ul data-component="ul">{children}</ul>;
|
||||
};
|
||||
|
||||
type LIProps = React.ClassAttributes<HTMLLIElement> &
|
||||
React.HTMLAttributes<HTMLLIElement> &
|
||||
ExtraProps;
|
||||
|
||||
export const li = (props: LIProps) => {
|
||||
const { children } = props;
|
||||
|
||||
return <li>{children}</li>;
|
||||
};
|
||||
|
||||
export const ol = (props: ULProps) => {
|
||||
const { children } = props;
|
||||
|
||||
return <ol data-component="ol">{children}</ol>;
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import React, { type Ref } from "react";
|
||||
import { Text } from "@appsmith/wds";
|
||||
import type { ExtraProps } from "react-markdown";
|
||||
|
||||
type ParagraphProps = React.ClassAttributes<HTMLDivElement> &
|
||||
React.HTMLAttributes<HTMLDivElement> &
|
||||
ExtraProps;
|
||||
|
||||
export const p = (props: ParagraphProps) => {
|
||||
const { children, ref } = props;
|
||||
|
||||
return (
|
||||
<Text
|
||||
color="neutral"
|
||||
data-component="p"
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
size="body"
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
.markdown {
|
||||
color: var(--color-fg);
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
/* This is required to remove the compensators of capsizing that comes up due to use of `wds-body-text` class */
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
table {
|
||||
border: var(--border-width-1) solid var(--color-bd);
|
||||
border-collapse: separate;
|
||||
border-radius: var(--border-radius-elevation-3);
|
||||
border-spacing: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
tr {
|
||||
display: table-row;
|
||||
vertical-align: inherit;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--inner-spacing-1) var(--inner-spacing-2);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: var(--border-width-1) solid var(--color-bd);
|
||||
border-right: var(--border-width-1) solid var(--color-bd);
|
||||
}
|
||||
|
||||
:is(td, th):last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--color-bg-neutral-subtle);
|
||||
}
|
||||
|
||||
thead:last-child tr:last-child th,
|
||||
tbody:last-child tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
[data-component="h1"] {
|
||||
margin-top: var(--inner-spacing-7);
|
||||
margin-bottom: var(--inner-spacing-4);
|
||||
}
|
||||
|
||||
[data-component="h2"] {
|
||||
margin-top: var(--inner-spacing-6);
|
||||
margin-bottom: var(--inner-spacing-3);
|
||||
}
|
||||
|
||||
[data-component="h3"] {
|
||||
margin-top: var(--inner-spacing-5);
|
||||
margin-bottom: var(--inner-spacing-3);
|
||||
}
|
||||
|
||||
[data-component="h4"] {
|
||||
margin-top: var(--inner-spacing-4);
|
||||
margin-bottom: var(--inner-spacing-2);
|
||||
}
|
||||
|
||||
[data-component="h5"] {
|
||||
margin-top: var(--inner-spacing-3);
|
||||
margin-bottom: var(--inner-spacing-2);
|
||||
}
|
||||
|
||||
[data-component="h6"] {
|
||||
margin-top: var(--inner-spacing-2);
|
||||
margin-bottom: var(--inner-spacing-1);
|
||||
}
|
||||
|
||||
[data-component="p"] {
|
||||
margin-bottom: var(--inner-spacing-4);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
:is(ul, ol) {
|
||||
margin-top: var(--inner-spacing-2);
|
||||
margin-bottom: var(--inner-spacing-4);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: var(--inner-spacing-2);
|
||||
margin-left: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-component="a"]:before,
|
||||
[data-component="a"]:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
[data-component="code"] {
|
||||
background-color: var(--color-bg-elevation-2);
|
||||
border-radius: var(--border-radius-elevation-3);
|
||||
outline: var(--border-width-1) solid var(--color-bg-neutral-subtle);
|
||||
margin-bottom: var(--spacing-2);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: var(--inner-spacing-4);
|
||||
margin-bottom: var(--inner-spacing-4);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: var(--inner-spacing-3);
|
||||
padding-right: var(--inner-spacing-3);
|
||||
margin-left: 0;
|
||||
border-left: var(--border-width-2) solid var(--color-bd-neutral);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { Options } from "react-markdown";
|
||||
|
||||
export interface MarkdownProps {
|
||||
/** The markdown content to render */
|
||||
children: string;
|
||||
/** Options for react-markdown */
|
||||
options?: Options;
|
||||
/** Additional CSS classes to apply to the component */
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { Markdown } from "@appsmith/wds";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta: Meta<typeof Markdown> = {
|
||||
title: "WDS/Widgets/Markdown",
|
||||
component: Markdown,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Markdown>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: `# Hello, Markdown!
|
||||
|
||||
This is a paragraph with **bold** and *italic* text.
|
||||
|
||||
## Code Example
|
||||
|
||||
\`\`\`javascript
|
||||
const greeting = "Hello, World!";
|
||||
console.log(greeting);
|
||||
\`\`\`
|
||||
|
||||
- List item 1
|
||||
- List item 2
|
||||
- List item 3
|
||||
- List item 3.1
|
||||
- List item 3.2
|
||||
- List item 3.3
|
||||
- List item 3.3.1
|
||||
- List item 3.3.2
|
||||
|
||||
1. List item 1
|
||||
2. List item 2
|
||||
3. List item 3
|
||||
|
||||
[Visit Appsmith](https://www.appsmith.com)
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
## Table Example
|
||||
|
||||
| Column 1 | Column 2 | Column 3 |
|
||||
|----------|----------|----------|
|
||||
| Row 1, Cell 1 | Row 1, Cell 2 | Row 1, Cell 3 |
|
||||
| Row 2, Cell 1 | Row 2, Cell 2 | Row 2, Cell 3 |
|
||||
| Row 3, Cell 1 | Row 3, Cell 2 | Row 3, Cell 3 |
|
||||
|
||||
## Blockquote Example
|
||||
|
||||
> This is a blockquote.
|
||||
>
|
||||
> It can span multiple lines.
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
|
@ -9,7 +9,7 @@ import React, { forwardRef } from "react";
|
|||
import type { TextProps } from "./types";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const _Text = (props: TextProps, ref: Ref<HTMLParagraphElement>) => {
|
||||
const _Text = (props: TextProps, ref: Ref<HTMLDivElement>) => {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export * from "./components/ListBox";
|
|||
export * from "./components/ListBoxItem";
|
||||
export * from "./components/MenuItem";
|
||||
export * from "./components/ChatInput";
|
||||
export * from "./components/Avatar";
|
||||
export * from "./components/Markdown";
|
||||
|
||||
export * from "./utils";
|
||||
export * from "./hooks";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
background-color: var(--color-bg-elevation-1) !important;
|
||||
}
|
||||
|
||||
code,
|
||||
code:not(:has(span)),
|
||||
.css-o1d7ko {
|
||||
background-color: var(--color-bg-accent-subtle) !important;
|
||||
color: var(--color-fg-on-accent-subtle) !important;
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@ import React from "react";
|
|||
jest.mock("react-markdown", () => (props: { children: unknown }) => {
|
||||
return <>{props.children}</>;
|
||||
});
|
||||
|
||||
jest.mock("remark-gfm", () => () => {
|
||||
})
|
||||
|
|
@ -257,6 +257,7 @@ __metadata:
|
|||
react-aria-components: ^1.2.1
|
||||
react-markdown: ^9.0.1
|
||||
react-syntax-highlighter: ^15.5.0
|
||||
remark-gfm: ^4.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||
languageName: unknown
|
||||
|
|
@ -17791,6 +17792,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escape-string-regexp@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "escape-string-regexp@npm:5.0.0"
|
||||
checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "escodegen@npm:2.1.0"
|
||||
|
|
@ -23933,6 +23941,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"markdown-table@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "markdown-table@npm:3.0.3"
|
||||
checksum: 8fcd3d9018311120fbb97115987f8b1665a603f3134c93fbecc5d1463380c8036f789e2a62c19432058829e594fff8db9ff81c88f83690b2f8ed6c074f8d9e10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"markdown-to-jsx@npm:^7.4.5":
|
||||
version: 7.4.7
|
||||
resolution: "markdown-to-jsx@npm:7.4.7"
|
||||
|
|
@ -23969,6 +23984,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-find-and-replace@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "mdast-util-find-and-replace@npm:3.0.1"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
escape-string-regexp: ^5.0.0
|
||||
unist-util-is: ^6.0.0
|
||||
unist-util-visit-parents: ^6.0.0
|
||||
checksum: 05d5c4ff02e31db2f8a685a13bcb6c3f44e040bd9dfa54c19a232af8de5268334c8755d79cb456ed4cced1300c4fb83e88444c7ae8ee9ff16869a580f29d08cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-from-markdown@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "mdast-util-from-markdown@npm:2.0.1"
|
||||
|
|
@ -23989,6 +24016,83 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-autolink-literal@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "mdast-util-gfm-autolink-literal@npm:2.0.1"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
ccount: ^2.0.0
|
||||
devlop: ^1.0.0
|
||||
mdast-util-find-and-replace: ^3.0.0
|
||||
micromark-util-character: ^2.0.0
|
||||
checksum: 5630b12e072d7004cb132231c94f667fb5813486779cb0dfb0a196d7ae0e048897a43b0b37e080017adda618ddfcbea1d7bf23c0fa31c87bfc683e0898ea1cfe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-footnote@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-footnote@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
devlop: ^1.1.0
|
||||
mdast-util-from-markdown: ^2.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
micromark-util-normalize-identifier: ^2.0.0
|
||||
checksum: 45d26b40e7a093712e023105791129d76e164e2168d5268e113298a22de30c018162683fb7893cdc04ab246dac0087eed708b2a136d1d18ed2b32b3e0cae4a79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-strikethrough@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-strikethrough@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
mdast-util-from-markdown: ^2.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
checksum: fe9b1d0eba9b791ff9001c008744eafe3dd7a81b085f2bf521595ce4a8e8b1b44764ad9361761ad4533af3e5d913d8ad053abec38172031d9ee32a8ebd1c7dbd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-table@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-table@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
devlop: ^1.0.0
|
||||
markdown-table: ^3.0.0
|
||||
mdast-util-from-markdown: ^2.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
checksum: 063a627fd0993548fd63ca0c24c437baf91ba7d51d0a38820bd459bc20bf3d13d7365ef8d28dca99176dd5eb26058f7dde51190479c186dfe6af2e11202957c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-task-list-item@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-task-list-item@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
devlop: ^1.0.0
|
||||
mdast-util-from-markdown: ^2.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
checksum: 37db90c59b15330fc54d790404abf5ef9f2f83e8961c53666fe7de4aab8dd5e6b3c296b6be19797456711a89a27840291d8871ff0438e9b4e15c89d170efe072
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "mdast-util-gfm@npm:3.0.0"
|
||||
dependencies:
|
||||
mdast-util-from-markdown: ^2.0.0
|
||||
mdast-util-gfm-autolink-literal: ^2.0.0
|
||||
mdast-util-gfm-footnote: ^2.0.0
|
||||
mdast-util-gfm-strikethrough: ^2.0.0
|
||||
mdast-util-gfm-table: ^2.0.0
|
||||
mdast-util-gfm-task-list-item: ^2.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
checksum: 62039d2f682ae3821ea1c999454863d31faf94d67eb9b746589c7e136076d7fb35fabc67e02f025c7c26fd7919331a0ee1aabfae24f565d9a6a9ebab3371c626
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-mdx-expression@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "mdast-util-mdx-expression@npm:2.0.1"
|
||||
|
|
@ -24232,6 +24336,99 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-autolink-literal@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0"
|
||||
dependencies:
|
||||
micromark-util-character: ^2.0.0
|
||||
micromark-util-sanitize-uri: ^2.0.0
|
||||
micromark-util-symbol: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: e00a570c70c837b9cbbe94b2c23b787f44e781cd19b72f1828e3453abca2a9fb600fa539cdc75229fa3919db384491063645086e02249481e6ff3ec2c18f767c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-footnote@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "micromark-extension-gfm-footnote@npm:2.1.0"
|
||||
dependencies:
|
||||
devlop: ^1.0.0
|
||||
micromark-core-commonmark: ^2.0.0
|
||||
micromark-factory-space: ^2.0.0
|
||||
micromark-util-character: ^2.0.0
|
||||
micromark-util-normalize-identifier: ^2.0.0
|
||||
micromark-util-sanitize-uri: ^2.0.0
|
||||
micromark-util-symbol: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: ac6fb039e98395d37b71ebff7c7a249aef52678b5cf554c89c4f716111d4be62ef99a5d715a5bd5d68fa549778c977d85cb671d1d8506dc8a3a1b46e867ae52f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-strikethrough@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "micromark-extension-gfm-strikethrough@npm:2.1.0"
|
||||
dependencies:
|
||||
devlop: ^1.0.0
|
||||
micromark-util-chunked: ^2.0.0
|
||||
micromark-util-classify-character: ^2.0.0
|
||||
micromark-util-resolve-all: ^2.0.0
|
||||
micromark-util-symbol: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: cdb7a38dd6eefb6ceb6792a44a6796b10f951e8e3e45b8579f599f43e7ae26ccd048c0aa7e441b3c29dd0c54656944fe6eb0098de2bc4b5106fbc0a42e9e016c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-table@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "micromark-extension-gfm-table@npm:2.1.0"
|
||||
dependencies:
|
||||
devlop: ^1.0.0
|
||||
micromark-factory-space: ^2.0.0
|
||||
micromark-util-character: ^2.0.0
|
||||
micromark-util-symbol: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: 249d695f5f8bd222a0d8a774ec78ea2a2d624cb50a4d008092a54aa87dad1f9d540e151d29696cf849eb1cee380113c4df722aebb3b425a214832a2de5dea1d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-tagfilter@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0"
|
||||
dependencies:
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: cf21552f4a63592bfd6c96ae5d64a5f22bda4e77814e3f0501bfe80e7a49378ad140f827007f36044666f176b3a0d5fea7c2e8e7973ce4b4579b77789f01ae95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm-task-list-item@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "micromark-extension-gfm-task-list-item@npm:2.1.0"
|
||||
dependencies:
|
||||
devlop: ^1.0.0
|
||||
micromark-factory-space: ^2.0.0
|
||||
micromark-util-character: ^2.0.0
|
||||
micromark-util-symbol: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: b1ad86a4e9d68d9ad536d94fb25a5182acbc85cc79318f4a6316034342f6a71d67983cc13f12911d0290fd09b2bda43cdabe8781a2d9cca2ebe0d421e8b2b8a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-extension-gfm@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "micromark-extension-gfm@npm:3.0.0"
|
||||
dependencies:
|
||||
micromark-extension-gfm-autolink-literal: ^2.0.0
|
||||
micromark-extension-gfm-footnote: ^2.0.0
|
||||
micromark-extension-gfm-strikethrough: ^2.0.0
|
||||
micromark-extension-gfm-table: ^2.0.0
|
||||
micromark-extension-gfm-tagfilter: ^2.0.0
|
||||
micromark-extension-gfm-task-list-item: ^2.0.0
|
||||
micromark-util-combine-extensions: ^2.0.0
|
||||
micromark-util-types: ^2.0.0
|
||||
checksum: 2060fa62666a09532d6b3a272d413bc1b25bbb262f921d7402795ac021e1362c8913727e33d7528d5b4ccaf26922ec51208c43f795a702964817bc986de886c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromark-factory-destination@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "micromark-factory-destination@npm:2.0.0"
|
||||
|
|
@ -29372,6 +29569,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remark-gfm@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "remark-gfm@npm:4.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
mdast-util-gfm: ^3.0.0
|
||||
micromark-extension-gfm: ^3.0.0
|
||||
remark-parse: ^11.0.0
|
||||
remark-stringify: ^11.0.0
|
||||
unified: ^11.0.0
|
||||
checksum: 84bea84e388061fbbb697b4b666089f5c328aa04d19dc544c229b607446bc10902e46b67b9594415a1017bbbd7c811c1f0c30d36682c6d1a6718b66a1558261b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remark-parse@npm:^11.0.0":
|
||||
version: 11.0.0
|
||||
resolution: "remark-parse@npm:11.0.0"
|
||||
|
|
@ -29397,6 +29608,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remark-stringify@npm:^11.0.0":
|
||||
version: 11.0.0
|
||||
resolution: "remark-stringify@npm:11.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": ^4.0.0
|
||||
mdast-util-to-markdown: ^2.0.0
|
||||
unified: ^11.0.0
|
||||
checksum: 59e07460eb629d6c3b3c0f438b0b236e7e6858fd5ab770303078f5a556ec00354d9c7fb9ef6d5f745a4617ac7da1ab618b170fbb4dac120e183fecd9cc86bce6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remixicon-react@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "remixicon-react@npm:1.0.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user