diff --git a/packages/paste-core/components/ai-log/__tests__/index.spec.tsx b/packages/paste-core/components/ai-log/__tests__/index.spec.tsx new file mode 100644 index 0000000000..3383edf6bd --- /dev/null +++ b/packages/paste-core/components/ai-log/__tests__/index.spec.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import {render} from '@testing-library/react'; + +import {AiLog} from '../src'; + +describe('AiLog', () => { + it('should render', () => { + const {getByText} = render(test); + expect(getByText('test')).toBeDefined(); + }); +}); diff --git a/packages/paste-core/components/ai-log/build.js b/packages/paste-core/components/ai-log/build.js new file mode 100644 index 0000000000..a4edeab49b --- /dev/null +++ b/packages/paste-core/components/ai-log/build.js @@ -0,0 +1,3 @@ +const {build} = require('../../../../tools/build/esbuild'); + +build(require('./package.json')); diff --git a/packages/paste-core/components/ai-log/package.json b/packages/paste-core/components/ai-log/package.json new file mode 100644 index 0000000000..8b487c5aef --- /dev/null +++ b/packages/paste-core/components/ai-log/package.json @@ -0,0 +1,59 @@ +{ + "name": "@twilio-paste/ai-log", + "version": "0.0.0", + "category": "data display", + "status": "production", + "description": "Ai chat log.", + "author": "Twilio Inc.", + "license": "MIT", + "main:dev": "src/index.tsx", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "dist/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean && NODE_ENV=production node build.js && tsc", + "build:js": "NODE_ENV=development node build.js", + "build:typedocs": "tsx ../../../../tools/build/generate-type-docs", + "clean": "rm -rf ./dist", + "tsc": "tsc" + }, + "peerDependencies": { + "@twilio-paste/animation-library": "^2.0.0", + "@twilio-paste/box": "^10.2.0", + "@twilio-paste/color-contrast-utils": "^5.0.0", + "@twilio-paste/customization": "^8.1.1", + "@twilio-paste/design-tokens": "^10.3.0", + "@twilio-paste/style-props": "^9.1.1", + "@twilio-paste/styling-library": "^3.0.0", + "@twilio-paste/theme": "^11.0.1", + "@twilio-paste/types": "^6.0.0", + "@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27", + "@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10", + "react": "^16.8.6 || ^17.0.2 || ^18.0.0", + "react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0" + }, + "devDependencies": { + "@twilio-paste/animation-library": "^2.0.0", + "@twilio-paste/box": "^10.2.0", + "@twilio-paste/color-contrast-utils": "^5.0.0", + "@twilio-paste/customization": "^8.1.1", + "@twilio-paste/design-tokens": "^10.3.0", + "@twilio-paste/style-props": "^9.1.1", + "@twilio-paste/styling-library": "^3.0.0", + "@twilio-paste/theme": "^11.0.1", + "@twilio-paste/types": "^6.0.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "tsx": "^3.12.10", + "typescript": "^4.9.4" + } +} diff --git a/packages/paste-core/components/ai-log/src/AIChatLog.tsx b/packages/paste-core/components/ai-log/src/AIChatLog.tsx new file mode 100644 index 0000000000..23a31253a9 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatLog.tsx @@ -0,0 +1,37 @@ +import { Box, safelySpreadBoxProps } from "@twilio-paste/box"; +import type { BoxProps } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export interface AIChatLogProps extends HTMLPasteProps<"div"> { + children?: React.ReactNode; + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * @default '{constantCase component-name}' + * @type {BoxProps['element']} + * @memberof AIChatLogProps + */ + element?: BoxProps["element"]; +} + +export const AIChatLog = React.forwardRef( + ({ element = "AI_LOG", children, ...props }, ref) => { + return ( + + + {children} + + + ); + }, +); + +AIChatLog.displayName = "AIChatLog"; diff --git a/packages/paste-core/components/ai-log/src/AIChatLogger.tsx b/packages/paste-core/components/ai-log/src/AIChatLogger.tsx new file mode 100644 index 0000000000..8424516062 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatLogger.tsx @@ -0,0 +1,58 @@ +import { animated, useReducedMotion, useTransition } from "@twilio-paste/animation-library"; +import { Box } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +import { AIChatLog } from "./AIChatLog"; +import type { AIChat } from "./useAIChatLogger"; + +const AnimatedAI = animated(Box); +type StyleProps = React.ComponentProps["style"]; + +export interface AIChatLoggerProps extends HTMLPasteProps<"div"> { + /** + * Array of AIs in the log. Use with useAIChatLogger() + * + * @default 'AI_ATTACHMENT' + * @type {BoxProps['element']} + * @memberof AIAttachmentProps + */ + AIs: AIChat[]; + children?: never; +} + +const buildTransitionX = (AIChat: AIChat): number => { + if (AIChat.variant === "ai") return -100; + if (AIChat.variant === "user") return 100; + return 0; +}; + +export const AIChatLogger = React.forwardRef(({ AIs, ...props }, ref) => { + const transitions = useTransition(AIs, { + keys: (AIChat: AIChat) => AIChat.id, + from: (AIChat: AIChat): StyleProps => ({ opacity: 0, x: buildTransitionX(AIChat) }), + enter: { opacity: 1, x: 0 }, + leave: (AIChat: AIChat): StyleProps => ({ opacity: 0, x: buildTransitionX(AIChat) }), + config: { + mass: 0.7, + tension: 190, + friction: 16, + }, + }); + + const animatedAIs = useReducedMotion() + ? AIs.map((AIChat) => React.cloneElement(AIChat.content, { key: AIChat.id })) + : transitions((styles: StyleProps, AIChat: AIChat, { key }: { key: string }) => ( + + {AIChat.content} + + )); + + return ( + + {animatedAIs} + + ); +}); + +AIChatLogger.displayName = "AIChatLogger"; diff --git a/packages/paste-core/components/ai-log/src/AIChatMessage.tsx b/packages/paste-core/components/ai-log/src/AIChatMessage.tsx new file mode 100644 index 0000000000..9f4b56e970 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatMessage.tsx @@ -0,0 +1,36 @@ +import { Box, safelySpreadBoxProps } from "@twilio-paste/box"; +import type { BoxElementProps, BoxStyleProps } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export interface AIChatMessageProps extends HTMLPasteProps<"div"> { + children?: React.ReactNode; + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * + * @default "CHAT_MESSAGE" + * @type {BoxProps["element"]} + * @memberof ChatMessageProps + */ + element?: BoxElementProps["element"]; +} + +export const AIChatMessage = React.forwardRef( + ({ children, element = "CHAT_MESSAGE", ...props }, ref) => { + return ( + + {children} + + ); + }, +); + +AIChatMessage.displayName = "AIChatMessage"; diff --git a/packages/paste-core/components/ai-log/src/AIChatMessageContent.tsx b/packages/paste-core/components/ai-log/src/AIChatMessageContent.tsx new file mode 100644 index 0000000000..0958389f62 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatMessageContent.tsx @@ -0,0 +1,40 @@ +import { Box, safelySpreadBoxProps } from "@twilio-paste/box"; +import type { BoxElementProps, BoxStyleProps } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export interface AIChatMessageContentProps extends HTMLPasteProps<"div"> { + children?: React.ReactNode; + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * + * @default "CHAT_BUBBLE" + * @type {BoxProps["element"]} + * @memberof AIChatBubbleProps + */ + element?: BoxElementProps["element"]; +} + +export const AIChatMessageContent = React.forwardRef( + ({ children, element = "CHAT_MESSAGE_CONTENT", ...props }, ref) => { + return ( + + {children} + + ); + }, +); + +AIChatMessageContent.displayName = "AIChatMessageContent"; diff --git a/packages/paste-core/components/ai-log/src/AIChatMessageMeta.tsx b/packages/paste-core/components/ai-log/src/AIChatMessageMeta.tsx new file mode 100644 index 0000000000..dd19c55fcc --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatMessageMeta.tsx @@ -0,0 +1,43 @@ +import { Box, safelySpreadBoxProps } from "@twilio-paste/box"; +import type { BoxElementProps } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export interface AIChatMessageMetaProps extends HTMLPasteProps<"div"> { + /** + * + * @default null + * @type {string} + * @memberof AIChatMessageMetaProps + */ + "aria-label": string; + children: NonNullable; + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * + * @default "CHAT_MESSAGE_META" + * @type {BoxProps["element"]} + * @memberof AIChatMessageMetaProps + */ + element?: BoxElementProps["element"]; +} + +export const AIChatMessageMeta = React.forwardRef( + ({ children, element = "CHAT_MESSAGE_META", ...props }, ref) => { + return ( + + {children} + + ); + }, +); + +AIChatMessageMeta.displayName = "AIChatMessageMeta"; diff --git a/packages/paste-core/components/ai-log/src/AIChatMessageMetaItem.tsx b/packages/paste-core/components/ai-log/src/AIChatMessageMetaItem.tsx new file mode 100644 index 0000000000..72b7e033d2 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/AIChatMessageMetaItem.tsx @@ -0,0 +1,47 @@ +import { Box, safelySpreadBoxProps } from "@twilio-paste/box"; +import type { BoxElementProps } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export interface AIChatMessageMetaItemProps extends HTMLPasteProps<"div"> { + children: NonNullable; + variant: "author" | "timestamp"; + /** + * Overrides the default element name to apply unique styles with the Customization Provider + * + * @default "CHAT_MESSAGE_META_ITEM" + * @type {BoxProps["element"]} + * @memberof AIChatMessageMetaItemProps + */ + element?: BoxElementProps["element"]; +} + +const variantStyles = { + author: { + color: "colorText", + lineHeight: "lineHeight50", + fontSize: "fontSize40", + }, + timestamp: {}, +}; + +export const AIChatMessageMetaItem = React.forwardRef( + ({ children, element = "CHAT_MESSAGE_META_ITEM", ...props }, ref) => ( + + {children} + + ), +); + +AIChatMessageMetaItem.displayName = "AIChatMessageMetaItem"; diff --git a/packages/paste-core/components/ai-log/src/index.tsx b/packages/paste-core/components/ai-log/src/index.tsx new file mode 100644 index 0000000000..65e31ebf04 --- /dev/null +++ b/packages/paste-core/components/ai-log/src/index.tsx @@ -0,0 +1,15 @@ +export { AIChatMessage } from "./AIChatMessage"; +export type { AIChatMessageProps } from "./AIChatMessage"; +export { AIChatMessageMeta } from "./AIChatMessageMeta"; +export type { AIChatMessageMetaProps } from "./AIChatMessageMeta"; +export { AIChatMessageMetaItem } from "./AIChatMessageMetaItem"; +export type { AIChatMessageMetaItemProps } from "./AIChatMessageMetaItem"; +export { AIChatMessageContent } from "./AIChatMessageContent"; +export type { AIChatMessageContentProps } from "./AIChatMessageContent"; + +export { AIChatLog } from "./AIChatLog"; +export type { AIChatLogProps } from "./AIChatLog"; +export { useAIChatLogger } from "./useAIChatLogger"; +export type { UseAIChatLogger } from "./useAIChatLogger"; +export { AIChatLogger } from "./AIChatLogger"; +export type { AIChatLoggerProps } from "./AIChatLogger"; diff --git a/packages/paste-core/components/ai-log/src/useAIChatLogger.tsx b/packages/paste-core/components/ai-log/src/useAIChatLogger.tsx new file mode 100644 index 0000000000..6d785d5f6b --- /dev/null +++ b/packages/paste-core/components/ai-log/src/useAIChatLogger.tsx @@ -0,0 +1,39 @@ +import { uid } from "@twilio-paste/uid-library"; +import * as React from "react"; + +export type AIChat = { + id: string; + content: React.ReactElement; +}; + +export type PartialIDChat = Omit & Partial>; + +type PushAIChat = (chat: PartialIDChat) => void; +type PopAIChat = (id?: string) => void; + +export type UseAIChatLogger = (...initialChats: PartialIDChat[]) => { + aiChats: AIChat[]; + push: PushAIChat; + pop: PopAIChat; + clear: () => void; +}; + +const aiChatWithId = (chat: PartialIDChat): AIChat => ({ ...chat, id: chat.id || uid(chat.content) }); + +export const useAIChatLogger: UseAIChatLogger = (...initialChats) => { + const parsedInitialChats = React.useMemo(() => initialChats.map(aiChatWithId), [initialChats]); + + const [aiChats, setAIChats] = React.useState(parsedInitialChats); + + const push: PushAIChat = React.useCallback((next) => { + setAIChats((prev) => prev.concat(aiChatWithId(next))); + }, []); + + const pop: PopAIChat = React.useCallback((id) => { + setAIChats((prev) => (id ? prev.filter((chat) => chat.id !== id) : prev.slice(0, -1))); + }, []); + + const clear: () => void = React.useCallback(() => setAIChats([]), []); + + return { push, pop, aiChats, clear }; +}; diff --git a/packages/paste-core/components/ai-log/stories/index.stories.tsx b/packages/paste-core/components/ai-log/stories/index.stories.tsx new file mode 100644 index 0000000000..2f9fc10400 --- /dev/null +++ b/packages/paste-core/components/ai-log/stories/index.stories.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; + +import { Avatar } from "@twilio-paste/avatar"; +import { Box } from "@twilio-paste/box"; +import { ArtificialIntelligenceIcon } from "@twilio-paste/icons/esm/ArtificialIntelligenceIcon"; +import { useUID } from "@twilio-paste/uid-library"; +import { AIChatLog, AIChatMessage, AIChatMessageContent, AIChatMessageMeta, AIChatMessageMetaItem } from "../src"; + +// eslint-disable-next-line import/no-default-export +export default { + title: "Components/AI Log", + component: AIChatLog, +}; + +export const Default = (): React.ReactNode => { + const [showButton, setShowButton] = React.useState(true); + const chatBoxUniqueID = useUID(); + return ( + + + + + + + You + + + + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Deserunt delectus fuga, necessitatibus eligendi + iure adipisci facilis exercitationem officiis dolorem laborum, ex fugiat quisquam itaque, earum sit nesciunt + impedit repellat assumenda. + + + 30007 + 30007 + 30007 + + + Was this helpful? + + + + + + + AI Bot + + + + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Deserunt delectus fuga, necessitatibus eligendi + iure adipisci facilis exercitationem officiis dolorem laborum, ex fugiat quisquam itaque, earum sit nesciunt + impedit repellat assumenda. + + + 30007 + 30007 + 30007 + + + Was this helpful? + + + + + ); +}; diff --git a/packages/paste-core/components/ai-log/tsconfig.json b/packages/paste-core/components/ai-log/tsconfig.json new file mode 100644 index 0000000000..b5daed7034 --- /dev/null +++ b/packages/paste-core/components/ai-log/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + }, + "include": [ + "src/**/*", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 5d5fac83e8..e21441e8ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11216,6 +11216,42 @@ __metadata: languageName: unknown linkType: soft +"@twilio-paste/ai-log@workspace:packages/paste-core/components/ai-log": + version: 0.0.0-use.local + resolution: "@twilio-paste/ai-log@workspace:packages/paste-core/components/ai-log" + dependencies: + "@twilio-paste/animation-library": ^2.0.0 + "@twilio-paste/box": ^10.2.0 + "@twilio-paste/color-contrast-utils": ^5.0.0 + "@twilio-paste/customization": ^8.1.1 + "@twilio-paste/design-tokens": ^10.3.0 + "@twilio-paste/style-props": ^9.1.1 + "@twilio-paste/styling-library": ^3.0.0 + "@twilio-paste/theme": ^11.0.1 + "@twilio-paste/types": ^6.0.0 + "@types/react": ^18.0.27 + "@types/react-dom": ^18.0.10 + react: ^18.0.0 + react-dom: ^18.0.0 + tsx: ^3.12.10 + typescript: ^4.9.4 + peerDependencies: + "@twilio-paste/animation-library": ^2.0.0 + "@twilio-paste/box": ^10.2.0 + "@twilio-paste/color-contrast-utils": ^5.0.0 + "@twilio-paste/customization": ^8.1.1 + "@twilio-paste/design-tokens": ^10.3.0 + "@twilio-paste/style-props": ^9.1.1 + "@twilio-paste/styling-library": ^3.0.0 + "@twilio-paste/theme": ^11.0.1 + "@twilio-paste/types": ^6.0.0 + "@types/react": ^16.8.6 || ^17.0.2 || ^18.0.27 + "@types/react-dom": ^16.8.6 || ^17.0.2 || ^18.0.10 + react: ^16.8.6 || ^17.0.2 || ^18.0.0 + react-dom: ^16.8.6 || ^17.0.2 || ^18.0.0 + languageName: unknown + linkType: soft + "@twilio-paste/alert-dialog@^9.2.0, @twilio-paste/alert-dialog@workspace:packages/paste-core/components/alert-dialog": version: 0.0.0-use.local resolution: "@twilio-paste/alert-dialog@workspace:packages/paste-core/components/alert-dialog" @@ -41657,7 +41693,7 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.19, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.21, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.19, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -43669,6 +43705,23 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"tsx@npm:^3.12.10": + version: 3.14.0 + resolution: "tsx@npm:3.14.0" + dependencies: + esbuild: ~0.18.20 + fsevents: ~2.3.3 + get-tsconfig: ^4.7.2 + source-map-support: ^0.5.21 + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: afcef5d9b90b5800cf1ffb749e943f63042d78a4c0d9eef6e13e43f4ecab465d45e2c9812a2c515cbdc2ee913ff1cd01bf5c606a48013dd3ce2214a631b45557 + languageName: node + linkType: hard + "tsx@npm:^4.0.0": version: 4.6.2 resolution: "tsx@npm:4.6.2"