Skip to content

Commit

Permalink
feat(ai-chat-log): add new AI Chat Log components (#3927)
Browse files Browse the repository at this point in the history
  • Loading branch information
krisantrobus authored Jun 18, 2024
1 parent 52b8bad commit 3302f72
Show file tree
Hide file tree
Showing 39 changed files with 14,708 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/eighty-seas-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/ai-chat-log": major
"@twilio-paste/core": minor
---

[AIChatLog]: Added a new AIChatLog component to the library to display interactions between AI entities
6 changes: 6 additions & 0 deletions .changeset/forty-clouds-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/button-group": patch
"@twilio-paste/core": patch
---

[Button Group] allow unattached button groups to wrap to another line
6 changes: 6 additions & 0 deletions .changeset/pretty-melons-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/button": patch
"@twilio-paste/core": patch
---

[Button] Add border radius 20 to size="reset" buttons which can be overridden by passing a border radius token to variant="reset" and size="reset" buttons
5 changes: 5 additions & 0 deletions .changeset/seven-otters-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@twilio-paste/codemods": minor
---

[Codemods] new export (ai-chat-log)
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"/packages/paste-icons",
"/packages/paste-core/core-bundle",
"/packages/paste-core/components/account-switcher",
"/packages/paste-core/components/ai-chat-log",
"/packages/paste-core/components/alert",
"/packages/paste-core/components/alert-dialog",
"/packages/paste-core/components/anchor",
Expand Down
9 changes: 9 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
"AccountSwitcherItemRadio": "@twilio-paste/core/account-switcher",
"AccountSwitcherSeparator": "@twilio-paste/core/account-switcher",
"useAccountSwitcherState": "@twilio-paste/core/account-switcher",
"AIChatLog": "@twilio-paste/core/ai-chat-log",
"AIChatLogger": "@twilio-paste/core/ai-chat-log",
"AIChatMessage": "@twilio-paste/core/ai-chat-log",
"AIChatMessageActionCard": "@twilio-paste/core/ai-chat-log",
"AIChatMessageActionGroup": "@twilio-paste/core/ai-chat-log",
"AIChatMessageAuthor": "@twilio-paste/core/ai-chat-log",
"AIChatMessageBody": "@twilio-paste/core/ai-chat-log",
"AIChatMessageLoading": "@twilio-paste/core/ai-chat-log",
"useAIChatLogger": "@twilio-paste/core/ai-chat-log",
"Alert": "@twilio-paste/core/alert",
"AlertDialog": "@twilio-paste/core/alert-dialog",
"Anchor": "@twilio-paste/core/anchor",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { render, screen } from "@testing-library/react";
import * as React from "react";

import { AIChatLogger, AIChatMessage, AIChatMessageBody } from "../src";
import type { AIChat } from "../src/useAIChatLogger";

const chats: AIChat[] = [
{
id: "uid1",
variant: "bot",
content: (
<AIChatMessage variant="bot">
<AIChatMessageBody>hi</AIChatMessageBody>
</AIChatMessage>
),
},
{
id: "uid2",
variant: "user",
content: (
<AIChatMessage variant="user">
<AIChatMessageBody>hello</AIChatMessageBody>
</AIChatMessage>
),
},
];

describe("ChatLogger", () => {
it("should render", () => {
render(<AIChatLogger aiChats={chats} />);
expect(screen.getByRole("log")).toBeDefined();
expect(screen.getByRole("list")).toBeDefined();
expect(screen.getAllByRole("listitem")).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { render, screen } from "@testing-library/react";
import * as React from "react";

import {
AIChatLog,
AIChatMessage,
AIChatMessageActionCard,
AIChatMessageActionGroup,
AIChatMessageAuthor,
AIChatMessageBody,
AIChatMessageLoading,
} from "../src";

const ExampleAIChatLog: React.FC<React.PropsWithChildren> = () => (
<AIChatLog>
<AIChatMessage variant="bot">
<AIChatMessageAuthor aria-label="AI said" data-testid="author">
Good Bot
</AIChatMessageAuthor>
<AIChatMessageBody size="default">Lorem ipsum dolor.</AIChatMessageBody>
<AIChatMessageActionGroup data-testid="action_group">
<AIChatMessageActionCard aria-label="action" data-testid="action">
Is this helpful?
</AIChatMessageActionCard>
</AIChatMessageActionGroup>
<AIChatMessageLoading onStopLoading={() => {}} data-testid="loading" />
</AIChatMessage>
</AIChatLog>
);

const CustomExampleAIChatLog: React.FC<React.PropsWithChildren> = () => (
<AIChatLog element="FOO_AI_LOG">
<AIChatMessage variant="bot" element="FOO_AI_CHAT_MESSAGE">
<AIChatMessageAuthor aria-label="AI said" data-testid="author" element="FOO_AI_CHAT_MESSAGE_AUTHOR">
Good Bot
</AIChatMessageAuthor>
<AIChatMessageBody size="default" element="FOO_AI_CHAT_MESSAGE_BODY">
Lorem ipsum dolor.
</AIChatMessageBody>
<AIChatMessageActionGroup data-testid="action_group" element="FOO_AI_CHAT_MESSAGE_ACTION_GROUP">
<AIChatMessageActionCard
aria-label="Feedback form"
data-testid="action"
element="FOO_AI_CHAT_MESSAGE_ACTION_CARD"
>
Is this helpful?
</AIChatMessageActionCard>
</AIChatMessageActionGroup>
<AIChatMessageLoading onStopLoading={() => {}} data-testid="loading" element="FOO_AI_CHAT_MESSAGE_LOADING" />
</AIChatMessage>
</AIChatLog>
);

describe("AIChatLog", () => {
it("should render", () => {
render(<ExampleAIChatLog />);
expect(screen.getByRole("log")).toBeDefined();
expect(screen.getByRole("list")).toBeDefined();
});
});

describe("Customization", () => {
it("should set element data attribute", () => {
render(<ExampleAIChatLog />);

expect(screen.getByRole("log").getAttribute("data-paste-element")).toEqual("AI_CHAT_LOG");
expect(screen.getByRole("list").getAttribute("data-paste-element")).toEqual("AI_CHAT_LOG_LIST");
expect(screen.getAllByRole("listitem")[0].getAttribute("data-paste-element")).toEqual("AI_CHAT_MESSAGE");
expect(screen.getByTestId("author").getAttribute("data-paste-element")).toEqual("AI_CHAT_MESSAGE_AUTHOR");
expect(screen.getByTestId("author").firstElementChild?.getAttribute("data-paste-element")).toEqual(
"AI_CHAT_MESSAGE_AUTHOR_BOT_AVATAR",
);
expect(screen.getByText("Lorem ipsum dolor.").getAttribute("data-paste-element")).toEqual("AI_CHAT_MESSAGE_BODY");
expect(screen.getByTestId("action_group").getAttribute("data-paste-element")).toEqual(
"AI_CHAT_MESSAGE_ACTION_GROUP",
);
expect(screen.getByTestId("action").getAttribute("data-paste-element")).toEqual("AI_CHAT_MESSAGE_ACTION_CARD");
expect(screen.getByTestId("loading").getAttribute("data-paste-element")).toEqual("AI_CHAT_MESSAGE_LOADING");
expect(screen.getByTestId("loading").firstElementChild?.getAttribute("data-paste-element")).toEqual(
"AI_CHAT_MESSAGE_LOADING_SKELETON",
);
expect(screen.getByTestId("loading").lastElementChild?.getAttribute("data-paste-element")).toEqual(
"AI_CHAT_MESSAGE_LOADING_STOP_LOADING",
);
expect(
screen.getByTestId("loading").lastElementChild?.firstElementChild?.getAttribute("data-paste-element"),
).toEqual("AI_CHAT_MESSAGE_LOADING_STOP_BUTTON");
});

it("should set custom element data attribute", () => {
render(<CustomExampleAIChatLog />);

expect(screen.getByRole("log").getAttribute("data-paste-element")).toEqual("FOO_AI_LOG");
expect(screen.getByRole("list").getAttribute("data-paste-element")).toEqual("FOO_AI_LOG_LIST");
expect(screen.getAllByRole("listitem")[0].getAttribute("data-paste-element")).toEqual("FOO_AI_CHAT_MESSAGE");
expect(screen.getByTestId("author").getAttribute("data-paste-element")).toEqual("FOO_AI_CHAT_MESSAGE_AUTHOR");
expect(screen.getByTestId("author").firstElementChild?.getAttribute("data-paste-element")).toEqual(
"FOO_AI_CHAT_MESSAGE_AUTHOR_BOT_AVATAR",
);
expect(screen.getByText("Lorem ipsum dolor.").getAttribute("data-paste-element")).toEqual(
"FOO_AI_CHAT_MESSAGE_BODY",
);
expect(screen.getByTestId("action_group").getAttribute("data-paste-element")).toEqual(
"FOO_AI_CHAT_MESSAGE_ACTION_GROUP",
);
expect(screen.getByTestId("action").getAttribute("data-paste-element")).toEqual("FOO_AI_CHAT_MESSAGE_ACTION_CARD");
expect(screen.getByTestId("loading").getAttribute("data-paste-element")).toEqual("FOO_AI_CHAT_MESSAGE_LOADING");
expect(screen.getByTestId("loading").firstElementChild?.getAttribute("data-paste-element")).toEqual(
"FOO_AI_CHAT_MESSAGE_LOADING_SKELETON",
);
expect(screen.getByTestId("loading").lastElementChild?.getAttribute("data-paste-element")).toEqual(
"FOO_AI_CHAT_MESSAGE_LOADING_STOP_LOADING",
);
expect(
screen.getByTestId("loading").lastElementChild?.firstElementChild?.getAttribute("data-paste-element"),
).toEqual("FOO_AI_CHAT_MESSAGE_LOADING_STOP_BUTTON");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { act, renderHook } from "@testing-library/react";
import * as React from "react";

import { AIChatMessage, AIChatMessageBody, useAIChatLogger } from "../src";

const aiChat = {
id: "custom-id-123",
variant: "bot",
content: (
<AIChatMessage variant="bot">
<AIChatMessageBody>hi</AIChatMessageBody>
</AIChatMessage>
),
} as const;

describe("useAIChatLogger", () => {
it("returns expected result with defaults", () => {
const { result } = renderHook(() => useAIChatLogger());

expect(result.current).toMatchObject({
aiChats: [],
pop: expect.any(Function),
push: expect.any(Function),
});
});

it("returns expected result with initialization", () => {
const { result } = renderHook(() => useAIChatLogger(aiChat));

expect(result.current.aiChats).toHaveLength(1);
expect(result.current.pop).toBeInstanceOf(Function);
expect(result.current.push).toBeInstanceOf(Function);
expect(result.current.aiChats[0]).toMatchObject(aiChat);
});

describe("push", () => {
it("pushes new aiChats with an id", () => {
const { result } = renderHook(() => useAIChatLogger());
expect(result.current.aiChats).toHaveLength(0);

act(() => {
result.current.push(aiChat);
});

expect(result.current.aiChats).toHaveLength(1);
expect(result.current.aiChats[0]).toMatchObject(aiChat);
});

it("pushes new aiChats without an id", () => {
const { result } = renderHook(() => useAIChatLogger());
expect(result.current.aiChats).toHaveLength(0);

act(() => {
const chatWithoutCustomId = { ...aiChat, id: undefined };
result.current.push(chatWithoutCustomId);
});

expect(result.current.aiChats).toHaveLength(1);
expect(result.current.aiChats[0]).toMatchObject({
id: expect.stringMatching(/^uid/),
});
});
});

describe("pop", () => {
it("pops aiChats with an id", () => {
const { result } = renderHook(() => useAIChatLogger(aiChat));
expect(result.current.aiChats).toHaveLength(1);

act(() => {
result.current.pop(aiChat.id);
});

expect(result.current.aiChats).toHaveLength(0);
});

it("pops aiChats without an id", () => {
const { result } = renderHook(() => useAIChatLogger(aiChat));
expect(result.current.aiChats).toHaveLength(1);

act(() => {
result.current.pop();
});

expect(result.current.aiChats).toHaveLength(0);
});
});
});
3 changes: 3 additions & 0 deletions packages/paste-core/components/ai-chat-log/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { build } = require("../../../../tools/build/esbuild");

build(require("./package.json"));
79 changes: 79 additions & 0 deletions packages/paste-core/components/ai-chat-log/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"name": "@twilio-paste/ai-chat-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/anchor": "^12.1.0",
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/avatar": "^9.0.0",
"@twilio-paste/box": "^10.2.0",
"@twilio-paste/button": "^14.0.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.1.1",
"@twilio-paste/design-tokens": "^10.3.0",
"@twilio-paste/icons": "^12.0.0",
"@twilio-paste/screen-reader-only": "^13.0.0",
"@twilio-paste/skeleton-loader": "^6.1.0",
"@twilio-paste/spinner": "^14.0.0",
"@twilio-paste/stack": "^8.0.0",
"@twilio-paste/style-props": "^9.1.1",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/text": "^10.0.0",
"@twilio-paste/theme": "^11.0.1",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.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/anchor": "^12.1.0",
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/avatar": "^9.1.0",
"@twilio-paste/box": "^10.2.0",
"@twilio-paste/button": "^14.1.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.1.1",
"@twilio-paste/design-tokens": "^10.3.0",
"@twilio-paste/icons": "^12.2.1",
"@twilio-paste/screen-reader-only": "^13.1.1",
"@twilio-paste/skeleton-loader": "^6.1.0",
"@twilio-paste/spinner": "^14.1.1",
"@twilio-paste/stack": "^8.1.0",
"@twilio-paste/style-props": "^9.1.1",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/text": "^10.1.0",
"@twilio-paste/theme": "^11.0.1",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsx": "^4.0.0",
"typescript": "^4.9.4"
}
}
Loading

0 comments on commit 3302f72

Please sign in to comment.