From ede880a2cefbba7f51b302be4fa5ccce8d29cf8c Mon Sep 17 00:00:00 2001 From: amanape Date: Mon, 29 Apr 2024 11:55:49 +0300 Subject: [PATCH 01/18] initial commit --- frontend/src/components/chat/Message.test.tsx | 34 +++++++++++++++++++ frontend/src/components/chat/Message.tsx | 20 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 frontend/src/components/chat/Message.test.tsx create mode 100644 frontend/src/components/chat/Message.tsx diff --git a/frontend/src/components/chat/Message.test.tsx b/frontend/src/components/chat/Message.test.tsx new file mode 100644 index 00000000000..e03e258a98f --- /dev/null +++ b/frontend/src/components/chat/Message.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import React from "react"; +import ChatMessage from "./Message"; + +describe("Message", () => { + it("should render a user message", () => { + render(); + + expect(screen.getByText("Hello")).toBeInTheDocument(); + expect(screen.getByText("Hello")).toHaveClass("self-end"); // user message should be on the right side + }); + + it("should render an assistant message", () => { + render(); + + expect(screen.getByText("Hi")).toBeInTheDocument(); + expect(screen.getByText("Hi")).not.toHaveClass("self-end"); // assistant message should be on the left side + }); + + it("should render markdown content", () => { + render( + , + ); + + expect(screen.getByText("console.log('Hello')")).toBeInTheDocument(); + expect(screen.getByText("console.log('Hello')")).toHaveClass("language-js"); + }); +}); diff --git a/frontend/src/components/chat/Message.tsx b/frontend/src/components/chat/Message.tsx new file mode 100644 index 00000000000..3fc7d33e8d6 --- /dev/null +++ b/frontend/src/components/chat/Message.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +type Message = { + sender: "user" | "assistant"; + content: string; +}; + +interface MessageProps { + message: Message; +} + +function ChatMessage({ message }: MessageProps) { + return ( +
+ {message.content} +
+ ); +} + +export default ChatMessage; From 900dd3044ce3f6ada3eeda9ee1f1697cfe6813f9 Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 12:21:35 +0300 Subject: [PATCH 02/18] update tests and feat-markdown --- frontend/src/components/chat/Message.test.tsx | 8 ++++---- frontend/src/components/chat/Message.tsx | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/chat/Message.test.tsx b/frontend/src/components/chat/Message.test.tsx index e03e258a98f..2a8d1a877d9 100644 --- a/frontend/src/components/chat/Message.test.tsx +++ b/frontend/src/components/chat/Message.test.tsx @@ -7,15 +7,15 @@ describe("Message", () => { it("should render a user message", () => { render(); - expect(screen.getByText("Hello")).toBeInTheDocument(); - expect(screen.getByText("Hello")).toHaveClass("self-end"); // user message should be on the right side + expect(screen.getByTestId("chat-bubble")).toBeInTheDocument(); + expect(screen.getByTestId("chat-bubble")).toHaveClass("self-end"); // user message should be on the right side }); it("should render an assistant message", () => { render(); - expect(screen.getByText("Hi")).toBeInTheDocument(); - expect(screen.getByText("Hi")).not.toHaveClass("self-end"); // assistant message should be on the left side + expect(screen.getByTestId("chat-bubble")).toBeInTheDocument(); + expect(screen.getByTestId("chat-bubble")).not.toHaveClass("self-end"); // assistant message should be on the left side }); it("should render markdown content", () => { diff --git a/frontend/src/components/chat/Message.tsx b/frontend/src/components/chat/Message.tsx index 3fc7d33e8d6..f38e43ecb10 100644 --- a/frontend/src/components/chat/Message.tsx +++ b/frontend/src/components/chat/Message.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Markdown from "react-markdown"; type Message = { sender: "user" | "assistant"; @@ -11,8 +12,11 @@ interface MessageProps { function ChatMessage({ message }: MessageProps) { return ( -
- {message.content} +
+ {message.content}
); } From d8d6aafee4567da76f86592cd7b2600c80fac839 Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 12:22:12 +0300 Subject: [PATCH 03/18] rename --- .../components/chat/{Message.test.tsx => ChatMessage.test.tsx} | 2 +- frontend/src/components/chat/{Message.tsx => ChatMessage.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename frontend/src/components/chat/{Message.test.tsx => ChatMessage.test.tsx} (96%) rename frontend/src/components/chat/{Message.tsx => ChatMessage.tsx} (100%) diff --git a/frontend/src/components/chat/Message.test.tsx b/frontend/src/components/chat/ChatMessage.test.tsx similarity index 96% rename from frontend/src/components/chat/Message.test.tsx rename to frontend/src/components/chat/ChatMessage.test.tsx index 2a8d1a877d9..969e2328336 100644 --- a/frontend/src/components/chat/Message.test.tsx +++ b/frontend/src/components/chat/ChatMessage.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; import React from "react"; -import ChatMessage from "./Message"; +import ChatMessage from "./ChatMessage"; describe("Message", () => { it("should render a user message", () => { diff --git a/frontend/src/components/chat/Message.tsx b/frontend/src/components/chat/ChatMessage.tsx similarity index 100% rename from frontend/src/components/chat/Message.tsx rename to frontend/src/components/chat/ChatMessage.tsx From b5ea875afa9ff09126214819f85e505df72c4eb9 Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 12:35:17 +0300 Subject: [PATCH 04/18] introduce chat --- frontend/src/components/chat/Chat.test.tsx | 20 +++++++++++++++++++ frontend/src/components/chat/Chat.tsx | 18 +++++++++++++++++ .../src/components/chat/ChatMessage.test.tsx | 8 ++++---- frontend/src/components/chat/ChatMessage.tsx | 7 +------ frontend/src/components/chat/message.d.ts | 4 ++++ 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/chat/Chat.test.tsx create mode 100644 frontend/src/components/chat/Chat.tsx create mode 100644 frontend/src/components/chat/message.d.ts diff --git a/frontend/src/components/chat/Chat.test.tsx b/frontend/src/components/chat/Chat.test.tsx new file mode 100644 index 00000000000..99378a5cd6d --- /dev/null +++ b/frontend/src/components/chat/Chat.test.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import Chat from "./Chat"; + +const MESSAGES: Message[] = [ + { sender: "assistant", content: "Hello!" }, + { sender: "user", content: "Hi!" }, + { sender: "assistant", content: "How can I help you today?" }, +]; + +describe("Chat", () => { + it("should render chat messages", () => { + render(); + + const messages = screen.getAllByTestId("message"); + + expect(messages).toHaveLength(MESSAGES.length); + }); +}); diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx new file mode 100644 index 00000000000..c379b402f6e --- /dev/null +++ b/frontend/src/components/chat/Chat.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import ChatMessage from "./ChatMessage"; + +interface ChatProps { + messages: Message[]; +} + +function Chat({ messages }: ChatProps) { + return ( +
+ {messages.map((message, index) => ( + + ))} +
+ ); +} + +export default Chat; diff --git a/frontend/src/components/chat/ChatMessage.test.tsx b/frontend/src/components/chat/ChatMessage.test.tsx index 969e2328336..4e15c8c36e8 100644 --- a/frontend/src/components/chat/ChatMessage.test.tsx +++ b/frontend/src/components/chat/ChatMessage.test.tsx @@ -7,15 +7,15 @@ describe("Message", () => { it("should render a user message", () => { render(); - expect(screen.getByTestId("chat-bubble")).toBeInTheDocument(); - expect(screen.getByTestId("chat-bubble")).toHaveClass("self-end"); // user message should be on the right side + expect(screen.getByTestId("message")).toBeInTheDocument(); + expect(screen.getByTestId("message")).toHaveClass("self-end"); // user message should be on the right side }); it("should render an assistant message", () => { render(); - expect(screen.getByTestId("chat-bubble")).toBeInTheDocument(); - expect(screen.getByTestId("chat-bubble")).not.toHaveClass("self-end"); // assistant message should be on the left side + expect(screen.getByTestId("message")).toBeInTheDocument(); + expect(screen.getByTestId("message")).not.toHaveClass("self-end"); // assistant message should be on the left side }); it("should render markdown content", () => { diff --git a/frontend/src/components/chat/ChatMessage.tsx b/frontend/src/components/chat/ChatMessage.tsx index f38e43ecb10..8fd7840638a 100644 --- a/frontend/src/components/chat/ChatMessage.tsx +++ b/frontend/src/components/chat/ChatMessage.tsx @@ -1,11 +1,6 @@ import React from "react"; import Markdown from "react-markdown"; -type Message = { - sender: "user" | "assistant"; - content: string; -}; - interface MessageProps { message: Message; } @@ -13,7 +8,7 @@ interface MessageProps { function ChatMessage({ message }: MessageProps) { return (
{message.content} diff --git a/frontend/src/components/chat/message.d.ts b/frontend/src/components/chat/message.d.ts new file mode 100644 index 00000000000..6a3aa491678 --- /dev/null +++ b/frontend/src/components/chat/message.d.ts @@ -0,0 +1,4 @@ +type Message = { + sender: "user" | "assistant"; + content: string; +}; From d8ee7e88565849fb6b1e0c65c4a357a88858ab25 Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 13:29:28 +0300 Subject: [PATCH 05/18] initial commit --- .../components/chat/ChatInterface.test.tsx | 25 +++++++++++++++++++ .../src/components/chat/ChatInterface.tsx | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 frontend/src/components/chat/ChatInterface.test.tsx create mode 100644 frontend/src/components/chat/ChatInterface.tsx diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx new file mode 100644 index 00000000000..85eb791db45 --- /dev/null +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { act } from "react-dom/test-utils"; +import userEvent from "@testing-library/user-event"; +import ChatInterface from "./ChatInterface"; + +describe("ChatInterface", () => { + it("should render the messages and input", () => { + render(); + expect(screen.queryAllByTestId("message")).toHaveLength(0); + }); + + it("should render the new message the user has typed", () => { + render(); + + const input = screen.getByRole("textbox"); + + act(() => { + userEvent.type(input, "my message{enter}"); + }); + + expect(screen.getByText("my message")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx new file mode 100644 index 00000000000..edd12527b6b --- /dev/null +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import ChatInput from "../ChatInput"; +import Chat from "./Chat"; + +function ChatInterface() { + const [messages, setMessages] = React.useState([]); + + const handleSendMessage = (content: string) => { + const message: Message = { sender: "user", content }; + setMessages((prev) => [...prev, message]); + }; + + return ( +
+ + +
+ ); +} + +export default ChatInterface; From d538f1383a7e4ad6f3af1116c989a02f7b72c13a Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 15:45:53 +0300 Subject: [PATCH 06/18] extend chatinterface, add new store, and utilize new reducers --- frontend/src/App.tsx | 2 +- .../components/chat/ChatInterface.test.tsx | 43 +++++++++++++++++-- .../src/components/chat/ChatInterface.tsx | 14 ++++-- frontend/src/services/actions.ts | 6 +-- frontend/src/services/chatService.ts | 3 +- frontend/src/services/observations.ts | 4 +- frontend/src/state/chat.ts | 32 ++++++++++++++ frontend/src/store.ts | 2 + 8 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 frontend/src/state/chat.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 339268e92d1..bcd035f8e2d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import { useDisclosure } from "@nextui-org/react"; import React, { useEffect, useState } from "react"; import { Toaster } from "react-hot-toast"; import CogTooth from "#/assets/cog-tooth"; -import ChatInterface from "#/components/ChatInterface"; +import ChatInterface from "#/components/chat/ChatInterface"; import Errors from "#/components/Errors"; import { Container, Orientation } from "#/components/Resizable"; import Workspace from "#/components/Workspace"; diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 85eb791db45..bdf256483d0 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -1,18 +1,24 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { act } from "react-dom/test-utils"; import userEvent from "@testing-library/user-event"; +import { renderWithProviders } from "test-utils"; import ChatInterface from "./ChatInterface"; +import Socket from "#/services/socket"; +import ActionType from "#/types/ActionType"; +import { addAssistantMessage } from "#/state/chat"; + +const socketSpy = vi.spyOn(Socket, "send"); describe("ChatInterface", () => { it("should render the messages and input", () => { - render(); + renderWithProviders(); expect(screen.queryAllByTestId("message")).toHaveLength(0); }); it("should render the new message the user has typed", () => { - render(); + renderWithProviders(); const input = screen.getByRole("textbox"); @@ -22,4 +28,35 @@ describe("ChatInterface", () => { expect(screen.getByText("my message")).toBeInTheDocument(); }); + + it("should render user and assistant messages", () => { + const { store } = renderWithProviders(, { + preloadedState: { + tempChat: { + messages: [{ sender: "user", content: "Hello" }], + }, + }, + }); + + expect(screen.getAllByTestId("message")).toHaveLength(1); + expect(screen.getByText("Hello")).toBeInTheDocument(); + + act(() => { + store.dispatch(addAssistantMessage("Hello to you!")); + }); + + expect(screen.getAllByTestId("message")).toHaveLength(2); + expect(screen.getByText("Hello to you!")).toBeInTheDocument(); + }); + + it("should send the a user message event to the Socket", () => { + renderWithProviders(); + const input = screen.getByRole("textbox"); + act(() => { + userEvent.type(input, "my message{enter}"); + }); + + const event = { action: ActionType.START, args: { task: "my message" } }; + expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event)); + }); }); diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index edd12527b6b..9ff10a53498 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -1,13 +1,21 @@ import React from "react"; +import { useDispatch, useSelector } from "react-redux"; import ChatInput from "../ChatInput"; import Chat from "./Chat"; +import { RootState } from "#/store"; +import { addUserMessage } from "#/state/chat"; +import ActionType from "#/types/ActionType"; +import Socket from "#/services/socket"; function ChatInterface() { - const [messages, setMessages] = React.useState([]); + const { messages } = useSelector((state: RootState) => state.tempChat); + const dispatch = useDispatch(); const handleSendMessage = (content: string) => { - const message: Message = { sender: "user", content }; - setMessages((prev) => [...prev, message]); + dispatch(addUserMessage(content)); + const event = { action: ActionType.START, args: { task: content } }; + + Socket.send(JSON.stringify(event)); }; return ( diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts index f4551a89606..4639657de21 100644 --- a/frontend/src/services/actions.ts +++ b/frontend/src/services/actions.ts @@ -1,6 +1,6 @@ import { changeTaskState } from "#/state/agentSlice"; import { setScreenshotSrc, setUrl } from "#/state/browserSlice"; -import { appendAssistantMessage } from "#/state/chatSlice"; +import { addAssistantMessage } from "#/state/chat"; import { setCode, updatePath } from "#/state/codeSlice"; import { appendInput } from "#/state/commandSlice"; import { setPlan } from "#/state/planSlice"; @@ -27,10 +27,10 @@ const messageActions = { store.dispatch(setCode(content)); }, [ActionType.THINK]: (message: ActionMessage) => { - store.dispatch(appendAssistantMessage(message.args.thought)); + store.dispatch(addAssistantMessage(message.args.thought)); }, [ActionType.FINISH]: (message: ActionMessage) => { - store.dispatch(appendAssistantMessage(message.message)); + store.dispatch(addAssistantMessage(message.message)); }, [ActionType.RUN]: (message: ActionMessage) => { store.dispatch(appendInput(message.args.command)); diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts index 18d4042f2aa..99acef369f0 100644 --- a/frontend/src/services/chatService.ts +++ b/frontend/src/services/chatService.ts @@ -10,6 +10,7 @@ import ActionType from "#/types/ActionType"; import { SocketMessage } from "#/types/ResponseType"; import { ActionMessage } from "#/types/Message"; import Socket from "./socket"; +import { addUserMessage } from "#/state/chat"; export function sendChatMessage(message: string): void { store.dispatch(appendUserMessage(message)); @@ -27,7 +28,7 @@ export function sendChatMessageFromEvent(event: string | SocketMessage): void { data = event as ActionMessage; } if (data && data.args && data.args.task) { - store.dispatch(appendUserMessage(data.args.task)); + store.dispatch(addUserMessage(data.args.task)); } } catch (error) { // diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts index 39cafafd93f..ac7eb52bc61 100644 --- a/frontend/src/services/observations.ts +++ b/frontend/src/services/observations.ts @@ -1,9 +1,9 @@ -import { appendAssistantMessage } from "#/state/chatSlice"; import { setUrl, setScreenshotSrc } from "#/state/browserSlice"; import store from "#/store"; import { ObservationMessage } from "#/types/Message"; import { appendOutput } from "#/state/commandSlice"; import ObservationType from "#/types/ObservationType"; +import { addAssistantMessage } from "#/state/chat"; export function handleObservationMessage(message: ObservationMessage) { switch (message.observation) { @@ -19,7 +19,7 @@ export function handleObservationMessage(message: ObservationMessage) { } break; default: - store.dispatch(appendAssistantMessage(message.message)); + store.dispatch(addAssistantMessage(message.message)); break; } } diff --git a/frontend/src/state/chat.ts b/frontend/src/state/chat.ts new file mode 100644 index 00000000000..b46dad27d3d --- /dev/null +++ b/frontend/src/state/chat.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +type SliceState = { messages: Message[] }; + +const initialState: SliceState = { messages: [] }; + +export const chatSlice = createSlice({ + name: "chat", + initialState, + reducers: { + addUserMessage(state, action: PayloadAction) { + const message: Message = { + sender: "user", + content: action.payload, + }; + + state.messages.push(message); + }, + + addAssistantMessage(state, action: PayloadAction) { + const message: Message = { + sender: "assistant", + content: action.payload, + }; + + state.messages.push(message); + }, + }, +}); + +export const { addUserMessage, addAssistantMessage } = chatSlice.actions; +export default chatSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 05ea2fe509a..1d815704429 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -2,6 +2,7 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; import agentReducer from "./state/agentSlice"; import browserReducer from "./state/browserSlice"; import chatReducer from "./state/chatSlice"; +import chat from "./state/chat"; import codeReducer from "./state/codeSlice"; import commandReducer from "./state/commandSlice"; import errorsReducer from "./state/errorsSlice"; @@ -11,6 +12,7 @@ import taskReducer from "./state/taskSlice"; export const rootReducer = combineReducers({ browser: browserReducer, chat: chatReducer, + tempChat: chat, code: codeReducer, cmd: commandReducer, task: taskReducer, From 09a8083dc95fc2fb751f44a94d781b76db305d5b Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 16:21:16 +0300 Subject: [PATCH 07/18] improve styles, code markdown, and adjust styles --- frontend/src/components/ChatInput.tsx | 2 +- frontend/src/components/chat/Chat.tsx | 2 +- frontend/src/components/chat/ChatInterface.tsx | 11 +++++++++-- frontend/src/components/chat/ChatMessage.test.tsx | 6 ++++-- frontend/src/components/chat/ChatMessage.tsx | 14 +++++++++----- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/ChatInput.tsx b/frontend/src/components/ChatInput.tsx index b0e8ee6bfe5..568e43ef5ee 100644 --- a/frontend/src/components/ChatInput.tsx +++ b/frontend/src/components/ChatInput.tsx @@ -42,7 +42,7 @@ function ChatInput({ disabled, onSendMessage }: ChatInputProps) { onCompositionStart={() => setIsComposing(true)} onCompositionEnd={() => setIsComposing(false)} placeholder={t(I18nKey.CHAT_INTERFACE$INPUT_PLACEHOLDER)} - className="pt-2 pb-3 px-3" + className="pb-3 px-3" classNames={{ inputWrapper: "bg-neutral-700 border border-neutral-600 rounded-lg", input: "pr-16 text-neutral-400", diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx index c379b402f6e..4f82d1889ef 100644 --- a/frontend/src/components/chat/Chat.tsx +++ b/frontend/src/components/chat/Chat.tsx @@ -7,7 +7,7 @@ interface ChatProps { function Chat({ messages }: ChatProps) { return ( -
+
{messages.map((message, index) => ( ))} diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index 9ff10a53498..f3be4210f15 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useDispatch, useSelector } from "react-redux"; +import { IoMdChatbubbles } from "react-icons/io"; import ChatInput from "../ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; @@ -19,8 +20,14 @@ function ChatInterface() { }; return ( -
- +
+
+ + Chat +
+
+ +
); diff --git a/frontend/src/components/chat/ChatMessage.test.tsx b/frontend/src/components/chat/ChatMessage.test.tsx index 4e15c8c36e8..cfe9b2dd6b1 100644 --- a/frontend/src/components/chat/ChatMessage.test.tsx +++ b/frontend/src/components/chat/ChatMessage.test.tsx @@ -28,7 +28,9 @@ describe("Message", () => { />, ); - expect(screen.getByText("console.log('Hello')")).toBeInTheDocument(); - expect(screen.getByText("console.log('Hello')")).toHaveClass("language-js"); + // SyntaxHighlighter breaks the code blocks into "tokens" + expect(screen.getByText("console")).toBeInTheDocument(); + expect(screen.getByText("log")).toBeInTheDocument(); + expect(screen.getByText("'Hello'")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/chat/ChatMessage.tsx b/frontend/src/components/chat/ChatMessage.tsx index 8fd7840638a..9b94b26811b 100644 --- a/frontend/src/components/chat/ChatMessage.tsx +++ b/frontend/src/components/chat/ChatMessage.tsx @@ -1,17 +1,21 @@ import React from "react"; import Markdown from "react-markdown"; +import { twMerge } from "tailwind-merge"; +import { code } from "../markdown/code"; interface MessageProps { message: Message; } function ChatMessage({ message }: MessageProps) { + const className = twMerge( + "p-3 text-white max-w-[90%] overflow-y-auto rounded-lg", + message.sender === "user" ? "bg-neutral-700 self-end" : "bg-neutral-500", + ); + return ( -
- {message.content} +
+ {message.content}
); } From 9114a7c75fe9622de8b7add8e2718425f38ea4a2 Mon Sep 17 00:00:00 2001 From: amanape Date: Wed, 1 May 2024 17:01:59 +0300 Subject: [PATCH 08/18] update actions to use new reducers --- frontend/src/services/actions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts index 4956f310db2..dd7d9c29615 100644 --- a/frontend/src/services/actions.ts +++ b/frontend/src/services/actions.ts @@ -30,20 +30,20 @@ const messageActions = { store.dispatch(addAssistantMessage(message.args.thought)); }, [ActionType.TALK]: (message: ActionMessage) => { - store.dispatch(appendAssistantMessage(message.args.content)); + store.dispatch(addAssistantMessage(message.args.content)); }, [ActionType.FINISH]: (message: ActionMessage) => { store.dispatch(addAssistantMessage(message.message)); }, [ActionType.RUN]: (message: ActionMessage) => { if (message.args.thought) { - store.dispatch(appendAssistantMessage(message.args.thought)); + store.dispatch(addAssistantMessage(message.args.thought)); } store.dispatch(appendInput(message.args.command)); }, [ActionType.RUN_IPYTHON]: (message: ActionMessage) => { if (message.args.thought) { - store.dispatch(appendAssistantMessage(message.args.thought)); + store.dispatch(addAssistantMessage(message.args.thought)); } store.dispatch(appendInput(message.args.code)); }, From 73c653837254bbe108b4a2df7e697f549d5434ae Mon Sep 17 00:00:00 2001 From: amanape Date: Thu, 2 May 2024 14:11:39 +0300 Subject: [PATCH 09/18] scroll down for new messages --- frontend/src/components/chat/Chat.test.tsx | 19 ++++++++++++++++++- frontend/src/components/chat/Chat.tsx | 7 +++++++ .../components/chat/ChatInterface.test.tsx | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/chat/Chat.test.tsx b/frontend/src/components/chat/Chat.test.tsx index 99378a5cd6d..dcf4bf412eb 100644 --- a/frontend/src/components/chat/Chat.test.tsx +++ b/frontend/src/components/chat/Chat.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import Chat from "./Chat"; @@ -9,6 +9,8 @@ const MESSAGES: Message[] = [ { sender: "assistant", content: "How can I help you today?" }, ]; +HTMLElement.prototype.scrollIntoView = vi.fn(); + describe("Chat", () => { it("should render chat messages", () => { render(); @@ -17,4 +19,19 @@ describe("Chat", () => { expect(messages).toHaveLength(MESSAGES.length); }); + + it("should scroll to the newest message", () => { + const { rerender } = render(); + + const newMessages: Message[] = [ + ...MESSAGES, + { sender: "user", content: "Create a spaceship" }, + ]; + + act(() => { + rerender(); + }); + + expect(HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx index 4f82d1889ef..426fa95a503 100644 --- a/frontend/src/components/chat/Chat.tsx +++ b/frontend/src/components/chat/Chat.tsx @@ -6,11 +6,18 @@ interface ChatProps { } function Chat({ messages }: ChatProps) { + const endOfMessagesRef = React.useRef(null); + + React.useEffect(() => { + endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + return (
{messages.map((message, index) => ( ))} +
); } diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index bdf256483d0..26b99865c8b 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -11,6 +11,10 @@ import { addAssistantMessage } from "#/state/chat"; const socketSpy = vi.spyOn(Socket, "send"); +// This is for the scrollview ref in Chat.tsx +// TODO: Move this into test setup +HTMLElement.prototype.scrollIntoView = vi.fn(); + describe("ChatInterface", () => { it("should render the messages and input", () => { renderWithProviders(); From 4bc1f4ecd7ad2ebfbd9737a4763975e321bd741e Mon Sep 17 00:00:00 2001 From: amanape Date: Thu, 2 May 2024 19:56:35 +0300 Subject: [PATCH 10/18] add fade and action banner --- .../components/chat/ChatInterface.test.tsx | 22 ++++++++++++++++- .../src/components/chat/ChatInterface.tsx | 24 +++++++++++++++++-- frontend/src/state/chat.ts | 10 +++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 26b99865c8b..80094fa2577 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -8,6 +8,8 @@ import ChatInterface from "./ChatInterface"; import Socket from "#/services/socket"; import ActionType from "#/types/ActionType"; import { addAssistantMessage } from "#/state/chat"; +import AgentTaskState from "#/types/AgentTaskState"; +import { changeTaskState } from "#/state/agentSlice"; const socketSpy = vi.spyOn(Socket, "send"); @@ -18,7 +20,7 @@ HTMLElement.prototype.scrollIntoView = vi.fn(); describe("ChatInterface", () => { it("should render the messages and input", () => { renderWithProviders(); - expect(screen.queryAllByTestId("message")).toHaveLength(0); + expect(screen.queryAllByTestId("message")).toHaveLength(1); // initial welcome message only }); it("should render the new message the user has typed", () => { @@ -63,4 +65,22 @@ describe("ChatInterface", () => { const event = { action: ActionType.START, args: { task: "my message" } }; expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event)); }); + + it("should display a typing indicator when waiting for assistant response", () => { + const { store } = renderWithProviders(, { + preloadedState: { + tempChat: { + messages: [{ sender: "assistant", content: "Hello" }], + }, + }, + }); + + expect(screen.queryByTestId("typing")).not.toBeInTheDocument(); + + act(() => { + store.dispatch(changeTaskState(AgentTaskState.RUNNING)); + }); + + expect(screen.getByTestId("typing")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index f3be4210f15..99f45ee38cf 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -7,9 +7,24 @@ import { RootState } from "#/store"; import { addUserMessage } from "#/state/chat"; import ActionType from "#/types/ActionType"; import Socket from "#/services/socket"; +import AgentTaskState from "#/types/AgentTaskState"; + +function ActionBanner() { + return ( +
+
+

Working...

+
+ ); +} function ChatInterface() { const { messages } = useSelector((state: RootState) => state.tempChat); + const { curTaskState } = useSelector((state: RootState) => state.agent); + const dispatch = useDispatch(); const handleSendMessage = (content: string) => { @@ -25,8 +40,13 @@ function ChatInterface() { Chat
-
- +
+
+ +
+ {curTaskState === AgentTaskState.RUNNING && } + {/* Fade between messages and input */} +
diff --git a/frontend/src/state/chat.ts b/frontend/src/state/chat.ts index b46dad27d3d..155df27fcec 100644 --- a/frontend/src/state/chat.ts +++ b/frontend/src/state/chat.ts @@ -2,7 +2,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; type SliceState = { messages: Message[] }; -const initialState: SliceState = { messages: [] }; +const initialState: SliceState = { + messages: [ + { + content: + "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?", + sender: "assistant", + }, + ], +}; export const chatSlice = createSlice({ name: "chat", From 2e6afdfce474040361c49f04882ebe8a0ef2a6ba Mon Sep 17 00:00:00 2001 From: amanape Date: Thu, 2 May 2024 20:56:40 +0300 Subject: [PATCH 11/18] support with jupyter --- frontend/src/components/chat/ChatInterface.tsx | 14 ++++---------- frontend/src/services/chatService.ts | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index 99f45ee38cf..cb9f4561321 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -1,13 +1,11 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { IoMdChatbubbles } from "react-icons/io"; import ChatInput from "../ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; -import { addUserMessage } from "#/state/chat"; -import ActionType from "#/types/ActionType"; -import Socket from "#/services/socket"; import AgentTaskState from "#/types/AgentTaskState"; +import { sendChatMessage } from "#/services/chatService"; function ActionBanner() { return ( @@ -25,13 +23,9 @@ function ChatInterface() { const { messages } = useSelector((state: RootState) => state.tempChat); const { curTaskState } = useSelector((state: RootState) => state.agent); - const dispatch = useDispatch(); - const handleSendMessage = (content: string) => { - dispatch(addUserMessage(content)); - const event = { action: ActionType.START, args: { task: content } }; - - Socket.send(JSON.stringify(event)); + const isNewTask = curTaskState === AgentTaskState.INIT; + sendChatMessage(content, isNewTask); }; return ( diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts index f84b7960601..3683c004b15 100644 --- a/frontend/src/services/chatService.ts +++ b/frontend/src/services/chatService.ts @@ -13,7 +13,7 @@ import Socket from "./socket"; import { addUserMessage } from "#/state/chat"; export function sendChatMessage(message: string, isTask: boolean = true): void { - store.dispatch(appendUserMessage(message)); + store.dispatch(addUserMessage(message)); let event; if (isTask) { event = { action: ActionType.START, args: { task: message } }; From 7df3cd322925099ef6eb967832d80316fa2d4d1f Mon Sep 17 00:00:00 2001 From: amanape Date: Thu, 2 May 2024 21:05:58 +0300 Subject: [PATCH 12/18] refactor to pass tests --- .../components/chat/ChatInterface.test.tsx | 4 ++-- .../src/components/chat/ChatInterface.tsx | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 80094fa2577..9dfe6280d7b 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { screen } from "@testing-library/react"; +import { screen, waitFor } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { act } from "react-dom/test-utils"; import userEvent from "@testing-library/user-event"; @@ -23,7 +23,7 @@ describe("ChatInterface", () => { expect(screen.queryAllByTestId("message")).toHaveLength(1); // initial welcome message only }); - it("should render the new message the user has typed", () => { + it("should render the new message the user has typed", async () => { renderWithProviders(); const input = screen.getByRole("textbox"); diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index cb9f4561321..84552713f6c 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -1,11 +1,14 @@ import React from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { IoMdChatbubbles } from "react-icons/io"; import ChatInput from "../ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; import AgentTaskState from "#/types/AgentTaskState"; import { sendChatMessage } from "#/services/chatService"; +import { addUserMessage } from "#/state/chat"; +import ActionType from "#/types/ActionType"; +import Socket from "#/services/socket"; function ActionBanner() { return ( @@ -23,9 +26,19 @@ function ChatInterface() { const { messages } = useSelector((state: RootState) => state.tempChat); const { curTaskState } = useSelector((state: RootState) => state.agent); + const dispatch = useDispatch(); + const handleSendMessage = (content: string) => { - const isNewTask = curTaskState === AgentTaskState.INIT; - sendChatMessage(content, isNewTask); + dispatch(addUserMessage(content)); + + let event; + if (curTaskState === AgentTaskState.INIT) { + event = { action: ActionType.START, args: { task: content } }; + } else { + event = { action: ActionType.USER_MESSAGE, args: { content } }; + } + + Socket.send(JSON.stringify(event)); }; return ( From c7d2f38c54e0493dc5a00679fbfec2574c71034e Mon Sep 17 00:00:00 2001 From: amanape Date: Thu, 2 May 2024 21:06:20 +0300 Subject: [PATCH 13/18] remove unused import --- frontend/src/components/chat/ChatInterface.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index 84552713f6c..8649da34ab4 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -5,7 +5,6 @@ import ChatInput from "../ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; import AgentTaskState from "#/types/AgentTaskState"; -import { sendChatMessage } from "#/services/chatService"; import { addUserMessage } from "#/state/chat"; import ActionType from "#/types/ActionType"; import Socket from "#/services/socket"; From 5e7b311960c8e3b18fd628ea6971e430bf48806f Mon Sep 17 00:00:00 2001 From: amanape Date: Fri, 3 May 2024 16:11:02 +0300 Subject: [PATCH 14/18] remove outdated files/folders --- frontend/src/components/AgentControlBar.tsx | 2 +- frontend/src/components/ChatInterface.tsx | 140 ---------------- .../components/chat/ChatInterface.test.tsx | 8 +- .../src/components/chat/ChatInterface.tsx | 4 +- frontend/src/hooks/useTypingEffect.test.ts | 156 ------------------ frontend/src/hooks/useTypingEffect.ts | 79 --------- frontend/src/services/actions.ts | 2 +- frontend/src/services/chatService.ts | 19 +-- frontend/src/services/observations.ts | 2 +- frontend/src/state/chat.ts | 40 ----- frontend/src/state/chatSlice.ts | 97 ++++------- frontend/src/store.ts | 2 - 12 files changed, 38 insertions(+), 513 deletions(-) delete mode 100644 frontend/src/components/ChatInterface.tsx delete mode 100644 frontend/src/hooks/useTypingEffect.test.ts delete mode 100644 frontend/src/hooks/useTypingEffect.ts delete mode 100644 frontend/src/state/chat.ts diff --git a/frontend/src/components/AgentControlBar.tsx b/frontend/src/components/AgentControlBar.tsx index d73850803dd..331e14c4d9d 100644 --- a/frontend/src/components/AgentControlBar.tsx +++ b/frontend/src/components/AgentControlBar.tsx @@ -6,10 +6,10 @@ import PauseIcon from "#/assets/pause"; import PlayIcon from "#/assets/play"; import { changeTaskState } from "#/services/agentStateService"; import { clearMsgs } from "#/services/session"; -import { clearMessages } from "#/state/chatSlice"; import store, { RootState } from "#/store"; import AgentTaskAction from "#/types/AgentTaskAction"; import AgentTaskState from "#/types/AgentTaskState"; +import { clearMessages } from "#/state/chatSlice"; const TaskStateActionMap = { [AgentTaskAction.START]: AgentTaskState.RUNNING, diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx deleted file mode 100644 index ee08c01d2fb..00000000000 --- a/frontend/src/components/ChatInterface.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { IoMdChatbubbles } from "react-icons/io"; -import Markdown from "react-markdown"; -import { useSelector } from "react-redux"; -import { useTypingEffect } from "#/hooks/useTypingEffect"; -import AgentTaskState from "../types/AgentTaskState"; -import { - addAssistantMessageToChat, - sendChatMessage, - setTypingActive, - takeOneAndType, -} from "#/services/chatService"; -import { Message } from "#/state/chatSlice"; -import { RootState } from "#/store"; -import ChatInput from "./ChatInput"; -import { code } from "./markdown/code"; - -interface IChatBubbleProps { - msg: Message; -} - -/** - * @returns jsx - * - * component used for typing effect when assistant replies - * - * makes uses of useTypingEffect hook - * - */ -function TypingChat() { - const { typeThis } = useSelector((state: RootState) => state.chat); - - const messageContent = useTypingEffect([typeThis?.content], { - loop: false, - setTypingActive, - playbackRate: 0.099, - addAssistantMessageToChat, - takeOneAndType, - typeThis, - }); - - return ( -
-
-
-
{messageContent}
-
-
-
- ); -} - -function ChatBubble({ msg }: IChatBubbleProps): JSX.Element { - return ( -
-
-
-
- {msg?.content} -
-
-
-
- ); -} - -function MessageList(): JSX.Element { - const messagesEndRef = useRef(null); - const { typingActive, newChatSequence, typeThis } = useSelector( - (state: RootState) => state.chat, - ); - - const messageScroll = () => { - messagesEndRef.current?.scrollIntoView({ - behavior: "auto", - block: "end", - }); - }; - - useEffect(() => { - messageScroll(); - if (!typingActive) return; - - const interval = setInterval(() => { - messageScroll(); - }, 100); - - // eslint-disable-next-line consistent-return - return () => clearInterval(interval); - }, [newChatSequence, typingActive]); - - useEffect(() => { - if (typeThis.content === "") return; - - if (!typingActive) setTypingActive(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [typeThis]); - - return ( -
-
- {newChatSequence.map((msg, index) => ( - - ))} - {typingActive && } -
-
-
-
- ); -} - -function ChatInterface(): JSX.Element { - const { initialized } = useSelector((state: RootState) => state.task); - const { curTaskState } = useSelector((state: RootState) => state.agent); - - const onUserMessage = (msg: string) => { - const isNewTask = curTaskState === AgentTaskState.INIT; - sendChatMessage(msg, isNewTask); - }; - - return ( -
-
- - Chat -
- - -
- ); -} - -export default ChatInterface; diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 9dfe6280d7b..1e3685bd18d 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { screen, waitFor } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { act } from "react-dom/test-utils"; import userEvent from "@testing-library/user-event"; @@ -7,7 +7,7 @@ import { renderWithProviders } from "test-utils"; import ChatInterface from "./ChatInterface"; import Socket from "#/services/socket"; import ActionType from "#/types/ActionType"; -import { addAssistantMessage } from "#/state/chat"; +import { addAssistantMessage } from "#/state/chatSlice"; import AgentTaskState from "#/types/AgentTaskState"; import { changeTaskState } from "#/state/agentSlice"; @@ -38,7 +38,7 @@ describe("ChatInterface", () => { it("should render user and assistant messages", () => { const { store } = renderWithProviders(, { preloadedState: { - tempChat: { + chat: { messages: [{ sender: "user", content: "Hello" }], }, }, @@ -69,7 +69,7 @@ describe("ChatInterface", () => { it("should display a typing indicator when waiting for assistant response", () => { const { store } = renderWithProviders(, { preloadedState: { - tempChat: { + chat: { messages: [{ sender: "assistant", content: "Hello" }], }, }, diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index 8649da34ab4..d211465aadd 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -5,7 +5,7 @@ import ChatInput from "../ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; import AgentTaskState from "#/types/AgentTaskState"; -import { addUserMessage } from "#/state/chat"; +import { addUserMessage } from "#/state/chatSlice"; import ActionType from "#/types/ActionType"; import Socket from "#/services/socket"; @@ -22,7 +22,7 @@ function ActionBanner() { } function ChatInterface() { - const { messages } = useSelector((state: RootState) => state.tempChat); + const { messages } = useSelector((state: RootState) => state.chat); const { curTaskState } = useSelector((state: RootState) => state.agent); const dispatch = useDispatch(); diff --git a/frontend/src/hooks/useTypingEffect.test.ts b/frontend/src/hooks/useTypingEffect.test.ts deleted file mode 100644 index 18942d0b0a4..00000000000 --- a/frontend/src/hooks/useTypingEffect.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { renderHook, act } from "@testing-library/react"; -import { useTypingEffect } from "./useTypingEffect"; - -describe("useTypingEffect", () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.clearAllTimers(); - }); - - // This test fails because the hook improperly handles this case. - it.skip("should handle empty strings array", () => { - const { result } = renderHook(() => useTypingEffect([])); - - // Immediately check the result since there's nothing to type - expect(result.current).toBe("\u00A0"); // Non-breaking space - }); - - it("should type out a string correctly", () => { - const message = "Hello, world! This is a test message."; - - const { result } = renderHook(() => useTypingEffect([message])); - - // msg.length - 2 because the first two characters are typed immediately - // 100ms per character, 0.1 playbackRate - const msToRun = (message.length - 2) * 100 * 0.1; - - // Fast-forward time by to simulate typing message - act(() => { - vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing - }); - - expect(result.current).toBe(message.slice(0, -1)); - - act(() => { - vi.advanceTimersByTime(1); // include the last character - }); - - expect(result.current).toBe(message); - }); - - it("should type of a string correctly with a different playback rate", () => { - const message = "Hello, world! This is a test message."; - const playbackRate = 0.5; - - const { result } = renderHook(() => - useTypingEffect([message], { playbackRate }), - ); - - const msToRun = (message.length - 2) * 100 * playbackRate; - - act(() => { - vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing - }); - - expect(result.current).toBe(message.slice(0, -1)); - - act(() => { - vi.advanceTimersByTime(1); // include the last character - }); - - expect(result.current).toBe(message); - }); - - it("should loop through strings when multiple are provided", () => { - const messages = ["Hello", "World"]; - - const { result } = renderHook(() => useTypingEffect(messages)); - - const msToRunFirstString = messages[0].length * 100 * 0.1; - - // Fast-forward to end of first string - act(() => { - vi.advanceTimersByTime(msToRunFirstString); - }); - - expect(result.current).toBe(messages[0]); // Hello - - // Fast-forward through the delay and through the second string - act(() => { - // TODO: Improve to clarify the expected timing - vi.runAllTimers(); - }); - - expect(result.current).toBe(messages[1]); // World - }); - - it("should call setTypingActive with false when typing completes without loop", () => { - const setTypingActiveMock = vi.fn(); - - renderHook(() => - useTypingEffect(["Hello, world!", "This is a test message."], { - loop: false, - setTypingActive: setTypingActiveMock, - }), - ); - - expect(setTypingActiveMock).not.toHaveBeenCalled(); - - act(() => { - vi.runAllTimers(); - }); - - expect(setTypingActiveMock).toHaveBeenCalledWith(false); - expect(setTypingActiveMock).toHaveBeenCalledTimes(1); - }); - - it("should call addAssistantMessageToChat with the typeThis argument when typing completes without loop", () => { - const addAssistantMessageToChatMock = vi.fn(); - - renderHook(() => - useTypingEffect(["Hello, world!", "This is a test message."], { - loop: false, - // Note that only "Hello, world!" is typed out (the first string in the array) - typeThis: { content: "Hello, world!", sender: "assistant" }, - addAssistantMessageToChat: addAssistantMessageToChatMock, - }), - ); - - expect(addAssistantMessageToChatMock).not.toHaveBeenCalled(); - - act(() => { - vi.runAllTimers(); - }); - - expect(addAssistantMessageToChatMock).toHaveBeenCalledTimes(1); - expect(addAssistantMessageToChatMock).toHaveBeenCalledWith({ - content: "Hello, world!", - sender: "assistant", - }); - }); - - it("should call takeOneAndType when typing completes without loop", () => { - const takeOneAndTypeMock = vi.fn(); - - renderHook(() => - useTypingEffect(["Hello, world!", "This is a test message."], { - loop: false, - takeOneAndType: takeOneAndTypeMock, - }), - ); - - expect(takeOneAndTypeMock).not.toHaveBeenCalled(); - - act(() => { - vi.runAllTimers(); - }); - - expect(takeOneAndTypeMock).toHaveBeenCalledTimes(1); - }); - - // Implementation is not clear on how to handle this case - it.todo("should handle typing with loop"); -}); diff --git a/frontend/src/hooks/useTypingEffect.ts b/frontend/src/hooks/useTypingEffect.ts deleted file mode 100644 index af80eb098a0..00000000000 --- a/frontend/src/hooks/useTypingEffect.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useEffect, useState } from "react"; -import { Message } from "#/state/chatSlice"; -/** - * hook to be used for typing chat effect - */ -export const useTypingEffect = ( - strings: string[] = [""], - { - loop = false, - playbackRate = 0.1, - setTypingActive = () => {}, - addAssistantMessageToChat = () => {}, - takeOneAndType = () => {}, - typeThis = { content: "", sender: "assistant" }, - }: { - loop?: boolean; - playbackRate?: number; - setTypingActive?: (bool: boolean) => void; - addAssistantMessageToChat?: (msg: Message) => void; - takeOneAndType?: () => void; - typeThis?: Message; - } = { - loop: false, - playbackRate: 0.1, - setTypingActive: () => {}, - addAssistantMessageToChat: () => {}, - takeOneAndType: () => {}, - typeThis: { content: "", sender: "assistant" }, - }, -) => { - // eslint-disable-next-line prefer-const - let [{ stringIndex, characterIndex }, setState] = useState<{ - stringIndex: number; - characterIndex: number; - }>({ - stringIndex: 0, - characterIndex: 0, - }); - - let timeoutId: number; - const emulateKeyStroke = () => { - // eslint-disable-next-line no-plusplus - characterIndex++; - if (characterIndex === strings[stringIndex].length) { - characterIndex = 0; - // eslint-disable-next-line no-plusplus - stringIndex++; - if (stringIndex === strings.length) { - if (!loop) { - setTypingActive(false); - addAssistantMessageToChat(typeThis); - takeOneAndType(); - return; - } - stringIndex = 0; - } - timeoutId = window.setTimeout(emulateKeyStroke, 100 * playbackRate); - } else if (characterIndex === strings[stringIndex].length - 1) { - timeoutId = window.setTimeout(emulateKeyStroke, 2000 * playbackRate); - } else { - timeoutId = window.setTimeout(emulateKeyStroke, 100 * playbackRate); - } - setState({ - characterIndex, - stringIndex, - }); - }; - - useEffect(() => { - emulateKeyStroke(); - return () => { - window.clearTimeout(timeoutId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const nonBreakingSpace = "\u00A0"; - return strings[stringIndex].slice(0, characterIndex + 1) || nonBreakingSpace; -}; diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts index e449f8bc584..d4e8165c812 100644 --- a/frontend/src/services/actions.ts +++ b/frontend/src/services/actions.ts @@ -1,6 +1,6 @@ import { changeTaskState } from "#/state/agentSlice"; import { setScreenshotSrc, setUrl } from "#/state/browserSlice"; -import { addAssistantMessage } from "#/state/chat"; +import { addAssistantMessage } from "#/state/chatSlice"; import { setCode, updatePath } from "#/state/codeSlice"; import { appendInput } from "#/state/commandSlice"; import { appendJupyterInput } from "#/state/jupyterSlice"; diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts index 3683c004b15..2ac75cf1bb4 100644 --- a/frontend/src/services/chatService.ts +++ b/frontend/src/services/chatService.ts @@ -1,16 +1,9 @@ -import { - Message, - appendToNewChatSequence, - appendUserMessage, - takeOneTypeIt, - toggleTypingActive, -} from "#/state/chatSlice"; import store from "#/store"; import ActionType from "#/types/ActionType"; import { SocketMessage } from "#/types/ResponseType"; import { ActionMessage } from "#/types/Message"; import Socket from "./socket"; -import { addUserMessage } from "#/state/chat"; +import { addUserMessage } from "#/state/chatSlice"; export function sendChatMessage(message: string, isTask: boolean = true): void { store.dispatch(addUserMessage(message)); @@ -39,13 +32,3 @@ export function addChatMessageFromEvent(event: string | SocketMessage): void { // } } - -export function setTypingActive(bool: boolean): void { - store.dispatch(toggleTypingActive(bool)); -} -export function addAssistantMessageToChat(msg: Message): void { - store.dispatch(appendToNewChatSequence(msg)); -} -export function takeOneAndType(): void { - store.dispatch(takeOneTypeIt()); -} diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts index cb79d2b5908..2225693e2d5 100644 --- a/frontend/src/services/observations.ts +++ b/frontend/src/services/observations.ts @@ -4,7 +4,7 @@ import { ObservationMessage } from "#/types/Message"; import { appendOutput } from "#/state/commandSlice"; import { appendJupyterOutput } from "#/state/jupyterSlice"; import ObservationType from "#/types/ObservationType"; -import { addAssistantMessage } from "#/state/chat"; +import { addAssistantMessage } from "#/state/chatSlice"; export function handleObservationMessage(message: ObservationMessage) { switch (message.observation) { diff --git a/frontend/src/state/chat.ts b/frontend/src/state/chat.ts deleted file mode 100644 index 155df27fcec..00000000000 --- a/frontend/src/state/chat.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -type SliceState = { messages: Message[] }; - -const initialState: SliceState = { - messages: [ - { - content: - "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?", - sender: "assistant", - }, - ], -}; - -export const chatSlice = createSlice({ - name: "chat", - initialState, - reducers: { - addUserMessage(state, action: PayloadAction) { - const message: Message = { - sender: "user", - content: action.payload, - }; - - state.messages.push(message); - }, - - addAssistantMessage(state, action: PayloadAction) { - const message: Message = { - sender: "assistant", - content: action.payload, - }; - - state.messages.push(message); - }, - }, -}); - -export const { addUserMessage, addAssistantMessage } = chatSlice.actions; -export default chatSlice.reducer; diff --git a/frontend/src/state/chatSlice.ts b/frontend/src/state/chatSlice.ts index 292d606b7fe..1438103f912 100644 --- a/frontend/src/state/chatSlice.ts +++ b/frontend/src/state/chatSlice.ts @@ -1,86 +1,45 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -export type Message = { - content: string; - sender: "user" | "assistant"; +type SliceState = { messages: Message[] }; + +const initialState: SliceState = { + messages: [ + { + content: + "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?", + sender: "assistant", + }, + ], }; -const initialMessages: Message[] = [ - { - content: - "Hi! I'm OpenDevin, an AI Software Engineer. What would you like to build with me today?", - sender: "assistant", - }, -]; export const chatSlice = createSlice({ name: "chat", - initialState: { - messages: initialMessages, - typingActive: false, - userMessages: initialMessages, - assistantMessages: initialMessages, - assistantMessagesTypingQueue: [] as Message[], - newChatSequence: initialMessages, - typeThis: { content: "", sender: "assistant" } as Message, - }, + initialState, reducers: { - appendUserMessage: (state, action) => { - state.messages.push({ content: action.payload, sender: "user" }); - state.userMessages.push({ content: action.payload, sender: "user" }); - state.newChatSequence.push({ content: action.payload, sender: "user" }); - }, - appendAssistantMessage: (state, action) => { - state.messages.push({ content: action.payload, sender: "assistant" }); + addUserMessage(state, action: PayloadAction) { + const message: Message = { + sender: "user", + content: action.payload, + }; - if (state.assistantMessagesTypingQueue.length > 0 || state.typingActive) { - state.assistantMessagesTypingQueue.push({ - content: action.payload, - sender: "assistant", - }); - } else if ( - state.assistantMessagesTypingQueue.length === 0 && - !state.typingActive - ) { - state.typeThis = { - content: action.payload, - sender: "assistant", - }; - state.typingActive = true; - } + state.messages.push(message); }, - toggleTypingActive: (state, action) => { - state.typingActive = action.payload; - }, + addAssistantMessage(state, action: PayloadAction) { + const message: Message = { + sender: "assistant", + content: action.payload, + }; - appendToNewChatSequence: (state, action) => { - state.newChatSequence.push(action.payload); + state.messages.push(message); }, - takeOneTypeIt: (state) => { - if (state.assistantMessagesTypingQueue.length > 0) { - state.typeThis = state.assistantMessagesTypingQueue.shift() as Message; - } - }, - clearMessages: (state) => { - state.messages = initialMessages; - state.userMessages = initialMessages; - state.assistantMessages = initialMessages; - state.newChatSequence = initialMessages; - state.assistantMessagesTypingQueue = []; - state.typingActive = false; - state.typeThis = { content: "", sender: "assistant" }; + clearMessages(state) { + state.messages = []; }, }, }); -export const { - appendUserMessage, - appendAssistantMessage, - toggleTypingActive, - appendToNewChatSequence, - takeOneTypeIt, - clearMessages, -} = chatSlice.actions; - +export const { addUserMessage, addAssistantMessage, clearMessages } = + chatSlice.actions; export default chatSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 4fbd339a15c..f675f929ec5 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -2,7 +2,6 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; import agentReducer from "./state/agentSlice"; import browserReducer from "./state/browserSlice"; import chatReducer from "./state/chatSlice"; -import chat from "./state/chat"; import codeReducer from "./state/codeSlice"; import commandReducer from "./state/commandSlice"; import errorsReducer from "./state/errorsSlice"; @@ -13,7 +12,6 @@ import jupyterReducer from "./state/jupyterSlice"; export const rootReducer = combineReducers({ browser: browserReducer, chat: chatReducer, - tempChat: chat, code: codeReducer, cmd: commandReducer, task: taskReducer, From ee15928ebbc078738463b9a407661dd7641b994f Mon Sep 17 00:00:00 2001 From: amanape Date: Fri, 3 May 2024 19:39:50 +0300 Subject: [PATCH 15/18] create simple useTyping hook --- .../components/chat/ChatInterface.test.tsx | 5 +++ .../src/components/chat/ChatMessage.test.tsx | 5 +++ frontend/src/components/chat/ChatMessage.tsx | 5 ++- frontend/src/hooks/useTyping.test.ts | 41 +++++++++++++++++++ frontend/src/hooks/useTyping.ts | 22 ++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/useTyping.test.ts create mode 100644 frontend/src/hooks/useTyping.ts diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 1e3685bd18d..2bed842e787 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -11,6 +11,11 @@ import { addAssistantMessage } from "#/state/chatSlice"; import AgentTaskState from "#/types/AgentTaskState"; import { changeTaskState } from "#/state/agentSlice"; +// avoid typing side-effect +vi.mock("#/hooks/useTyping", () => ({ + useTyping: vi.fn((text: string) => text), +})); + const socketSpy = vi.spyOn(Socket, "send"); // This is for the scrollview ref in Chat.tsx diff --git a/frontend/src/components/chat/ChatMessage.test.tsx b/frontend/src/components/chat/ChatMessage.test.tsx index cfe9b2dd6b1..e385386cc9d 100644 --- a/frontend/src/components/chat/ChatMessage.test.tsx +++ b/frontend/src/components/chat/ChatMessage.test.tsx @@ -3,6 +3,11 @@ import { describe, it, expect } from "vitest"; import React from "react"; import ChatMessage from "./ChatMessage"; +// avoid typing side-effect +vi.mock("#/hooks/useTyping", () => ({ + useTyping: vi.fn((text: string) => text), +})); + describe("Message", () => { it("should render a user message", () => { render(); diff --git a/frontend/src/components/chat/ChatMessage.tsx b/frontend/src/components/chat/ChatMessage.tsx index 9b94b26811b..5490fb230e0 100644 --- a/frontend/src/components/chat/ChatMessage.tsx +++ b/frontend/src/components/chat/ChatMessage.tsx @@ -2,12 +2,15 @@ import React from "react"; import Markdown from "react-markdown"; import { twMerge } from "tailwind-merge"; import { code } from "../markdown/code"; +import { useTyping } from "#/hooks/useTyping"; interface MessageProps { message: Message; } function ChatMessage({ message }: MessageProps) { + const text = useTyping(message.content); + const className = twMerge( "p-3 text-white max-w-[90%] overflow-y-auto rounded-lg", message.sender === "user" ? "bg-neutral-700 self-end" : "bg-neutral-500", @@ -15,7 +18,7 @@ function ChatMessage({ message }: MessageProps) { return (
- {message.content} + {text}
); } diff --git a/frontend/src/hooks/useTyping.test.ts b/frontend/src/hooks/useTyping.test.ts new file mode 100644 index 00000000000..dfe0719fb5a --- /dev/null +++ b/frontend/src/hooks/useTyping.test.ts @@ -0,0 +1,41 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, it, vi } from "vitest"; +import { useTyping } from "./useTyping"; + +vi.useFakeTimers(); + +describe("useTyping", () => { + it("should 'type' a given message", () => { + const text = "Hello, World!"; + const typingSpeed = 10; + + const { result } = renderHook(() => useTyping(text)); + expect(result.current).toBe("H"); + + act(() => { + vi.advanceTimersByTime(typingSpeed); + }); + + expect(result.current).toBe("He"); + + act(() => { + vi.advanceTimersByTime(typingSpeed); + }); + + expect(result.current).toBe("Hel"); + + for (let i = 3; i < text.length; i += 1) { + act(() => { + vi.advanceTimersByTime(typingSpeed); + }); + } + + expect(result.current).toBe("Hello, World!"); + + act(() => { + vi.advanceTimersByTime(typingSpeed); + }); + + expect(result.current).toBe("Hello, World!"); + }); +}); diff --git a/frontend/src/hooks/useTyping.ts b/frontend/src/hooks/useTyping.ts new file mode 100644 index 00000000000..5012240cfb1 --- /dev/null +++ b/frontend/src/hooks/useTyping.ts @@ -0,0 +1,22 @@ +import React from "react"; + +export const useTyping = (text: string) => { + const [message, setMessage] = React.useState(text[0]); + + const advance = () => + setTimeout(() => { + if (message.length < text.length) { + setMessage(text.slice(0, message.length + 1)); + } + }, 10); + + React.useEffect(() => { + const timeout = advance(); + + return () => { + clearTimeout(timeout); + }; + }, [message]); + + return message; +}; From fdf7f5a9f5c5f1cd2bc632959b67d0e3b23faa47 Mon Sep 17 00:00:00 2001 From: amanape Date: Sat, 4 May 2024 18:30:38 +0300 Subject: [PATCH 16/18] remove action banner, extend tests, move files --- .../components/{ => chat}/ChatInput.test.tsx | 0 .../src/components/{ => chat}/ChatInput.tsx | 0 .../components/chat/ChatInterface.test.tsx | 35 +++++++++++-------- .../src/components/chat/ChatInterface.tsx | 18 ++-------- 4 files changed, 23 insertions(+), 30 deletions(-) rename frontend/src/components/{ => chat}/ChatInput.test.tsx (100%) rename frontend/src/components/{ => chat}/ChatInput.tsx (100%) diff --git a/frontend/src/components/ChatInput.test.tsx b/frontend/src/components/chat/ChatInput.test.tsx similarity index 100% rename from frontend/src/components/ChatInput.test.tsx rename to frontend/src/components/chat/ChatInput.test.tsx diff --git a/frontend/src/components/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx similarity index 100% rename from frontend/src/components/ChatInput.tsx rename to frontend/src/components/chat/ChatInput.tsx diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 2bed842e787..840b026c67e 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -8,8 +8,6 @@ import ChatInterface from "./ChatInterface"; import Socket from "#/services/socket"; import ActionType from "#/types/ActionType"; import { addAssistantMessage } from "#/state/chatSlice"; -import AgentTaskState from "#/types/AgentTaskState"; -import { changeTaskState } from "#/state/agentSlice"; // avoid typing side-effect vi.mock("#/hooks/useTyping", () => ({ @@ -22,14 +20,24 @@ const socketSpy = vi.spyOn(Socket, "send"); // TODO: Move this into test setup HTMLElement.prototype.scrollIntoView = vi.fn(); +const renderChatInterface = () => + renderWithProviders(, { + preloadedState: { + task: { + initialized: true, + completed: false, + }, + }, + }); + describe("ChatInterface", () => { it("should render the messages and input", () => { - renderWithProviders(); + renderChatInterface(); expect(screen.queryAllByTestId("message")).toHaveLength(1); // initial welcome message only }); it("should render the new message the user has typed", async () => { - renderWithProviders(); + renderChatInterface(); const input = screen.getByRole("textbox"); @@ -61,7 +69,7 @@ describe("ChatInterface", () => { }); it("should send the a user message event to the Socket", () => { - renderWithProviders(); + renderChatInterface(); const input = screen.getByRole("textbox"); act(() => { userEvent.type(input, "my message{enter}"); @@ -71,21 +79,18 @@ describe("ChatInterface", () => { expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event)); }); - it("should display a typing indicator when waiting for assistant response", () => { - const { store } = renderWithProviders(, { + it("should disable the user input if agent is not initialized", () => { + renderWithProviders(, { preloadedState: { - chat: { - messages: [{ sender: "assistant", content: "Hello" }], + task: { + initialized: false, + completed: false, }, }, }); - expect(screen.queryByTestId("typing")).not.toBeInTheDocument(); - - act(() => { - store.dispatch(changeTaskState(AgentTaskState.RUNNING)); - }); + const submitButton = screen.getByLabelText(/send message/i); - expect(screen.getByTestId("typing")).toBeInTheDocument(); + expect(submitButton).toBeDisabled(); }); }); diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index d211465aadd..40e742cd509 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { IoMdChatbubbles } from "react-icons/io"; -import ChatInput from "../ChatInput"; +import ChatInput from "./ChatInput"; import Chat from "./Chat"; import { RootState } from "#/store"; import AgentTaskState from "#/types/AgentTaskState"; @@ -9,19 +9,8 @@ import { addUserMessage } from "#/state/chatSlice"; import ActionType from "#/types/ActionType"; import Socket from "#/services/socket"; -function ActionBanner() { - return ( -
-
-

Working...

-
- ); -} - function ChatInterface() { + const { initialized } = useSelector((state: RootState) => state.task); const { messages } = useSelector((state: RootState) => state.chat); const { curTaskState } = useSelector((state: RootState) => state.agent); @@ -50,11 +39,10 @@ function ChatInterface() {
- {curTaskState === AgentTaskState.RUNNING && } {/* Fade between messages and input */}
- +
); } From 6c8bef84fdfb259b43b93553bb52048a6f8cfaa5 Mon Sep 17 00:00:00 2001 From: amanape Date: Sat, 4 May 2024 18:44:10 +0300 Subject: [PATCH 17/18] extend tests --- .../components/chat/ChatInterface.test.tsx | 41 ++++++++++++++++++- .../src/components/chat/ChatInterface.tsx | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx index 840b026c67e..37290af5e37 100644 --- a/frontend/src/components/chat/ChatInterface.test.tsx +++ b/frontend/src/components/chat/ChatInterface.test.tsx @@ -8,6 +8,7 @@ import ChatInterface from "./ChatInterface"; import Socket from "#/services/socket"; import ActionType from "#/types/ActionType"; import { addAssistantMessage } from "#/state/chatSlice"; +import AgentTaskState from "#/types/AgentTaskState"; // avoid typing side-effect vi.mock("#/hooks/useTyping", () => ({ @@ -68,8 +69,19 @@ describe("ChatInterface", () => { expect(screen.getByText("Hello to you!")).toBeInTheDocument(); }); - it("should send the a user message event to the Socket", () => { - renderChatInterface(); + it("should send the a start event to the Socket", () => { + renderWithProviders(, { + preloadedState: { + task: { + initialized: true, + completed: false, + }, + agent: { + curTaskState: AgentTaskState.INIT, + }, + }, + }); + const input = screen.getByRole("textbox"); act(() => { userEvent.type(input, "my message{enter}"); @@ -79,6 +91,31 @@ describe("ChatInterface", () => { expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event)); }); + it("should send the a user message event to the Socket", () => { + renderWithProviders(, { + preloadedState: { + task: { + initialized: true, + completed: false, + }, + agent: { + curTaskState: AgentTaskState.AWAITING_USER_INPUT, + }, + }, + }); + + const input = screen.getByRole("textbox"); + act(() => { + userEvent.type(input, "my message{enter}"); + }); + + const event = { + action: ActionType.USER_MESSAGE, + args: { message: "my message" }, + }; + expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event)); + }); + it("should disable the user input if agent is not initialized", () => { renderWithProviders(, { preloadedState: { diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx index 40e742cd509..36be99b1bbc 100644 --- a/frontend/src/components/chat/ChatInterface.tsx +++ b/frontend/src/components/chat/ChatInterface.tsx @@ -23,7 +23,7 @@ function ChatInterface() { if (curTaskState === AgentTaskState.INIT) { event = { action: ActionType.START, args: { task: content } }; } else { - event = { action: ActionType.USER_MESSAGE, args: { content } }; + event = { action: ActionType.USER_MESSAGE, args: { message: content } }; } Socket.send(JSON.stringify(event)); From 62faad60b16d10f7da4737ec1272825b34261aed Mon Sep 17 00:00:00 2001 From: amanape Date: Sat, 4 May 2024 19:30:34 +0300 Subject: [PATCH 18/18] disable error --- frontend/src/hooks/useTyping.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/hooks/useTyping.ts b/frontend/src/hooks/useTyping.ts index 5012240cfb1..fc4e2f7191c 100644 --- a/frontend/src/hooks/useTyping.ts +++ b/frontend/src/hooks/useTyping.ts @@ -16,6 +16,7 @@ export const useTyping = (text: string) => { return () => { clearTimeout(timeout); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [message]); return message;