Skip to content

Commit

Permalink
feat: update chat ui (#907)
Browse files Browse the repository at this point in the history
* feat: remove react-mentions

* feat: add chat header

* feat: remove v2/chat

* feat: add fature flag

* feat: add new chat UI

* feat: add prompt and brain name to messages
  • Loading branch information
mamadoudicko committed Aug 11, 2023
1 parent d4d19bb commit 80be40a
Show file tree
Hide file tree
Showing 41 changed files with 363 additions and 484 deletions.
20 changes: 9 additions & 11 deletions frontend/app/chat/[chatId]/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,18 @@ vi.mock("@/lib/context/BrainConfigProvider/brain-config-provider", () => ({
describe("Chat page", () => {
it("should render chat page correctly", () => {
const { getByTestId } = render(
<SupabaseProviderMock>
<BrainConfigProviderMock>
<BrainProviderMock>
<SelectedChatPage />,
</BrainProviderMock>
</BrainConfigProviderMock>
</SupabaseProviderMock>
<ChatProviderMock>
<SupabaseProviderMock>
<BrainConfigProviderMock>
<BrainProviderMock>
<SelectedChatPage />,
</BrainProviderMock>
</BrainConfigProviderMock>
</SupabaseProviderMock>
</ChatProviderMock>
);

expect(getByTestId("chat-page")).toBeDefined();
expect(getByTestId("chat-messages")).toBeDefined();
expect(getByTestId("chat-input")).toBeDefined();

expect(getByTestId("page-heading-title")).toBeDefined();
expect(getByTestId("page-heading-subtitle")).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChatInput } from "../ChatInput";

export const ActionsBar = (): JSX.Element => {
return (
<div className="flex mt-4 flex-row w-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-6">
<ChatInput />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type StyleMentionsInputProps = {
value: string;
onChange: (value: string) => void;
placeholder: string;
};

export const MentionsInput = ({
onChange,
placeholder,
value,
}: StyleMentionsInputProps): JSX.Element => {
return (
<input
autoFocus
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
value={value}
className="focus:outline-none focus:border-none"
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useState } from "react";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useActionsBar = () => {
const [value, setValue] = useState("");

const handleChange = (newPlainTextValue: string) => {
setValue(newPlainTextValue);
};

return {
handleChange,
value,
};
};
29 changes: 29 additions & 0 deletions frontend/app/chat/[chatId]/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useTranslation } from "react-i18next";

import { useChatContext } from "@/lib/context";

export const ChatHeader = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
const { history } = useChatContext();

if (history.length !== 0) {
return (
<h1 className="text-3xl font-bold text-center">
{t("chat_title_intro")}{" "}
<span className="text-purple-500">{t("brains")}</span>
</h1>
);
}

return (
<h1 className="text-3xl font-bold text-center">
{t("chat_title_intro")}{" "}
<span className="text-purple-500">{t("brains")}</span>
{" !! "}
<br />
{t("empty_brain_title_prefix")}{" "}
<span className="text-purple-500">{t("brain")}</span>{" "}
{t("empty_brain_title_suffix")}
</h1>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState } from "react";

import { useChat } from "../../../hooks/useChat";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useChatInput = () => {
const [message, setMessage] = useState<string>("");
const { addQuestion, generatingAnswer, chatId } = useChat();

const submitQuestion = () => {
if (message.length === 0) {
return;
}
if (!generatingAnswer) {
void addQuestion(message, () => setMessage(""));
}
};

return {
message,
setMessage,
submitQuestion,
generatingAnswer,
chatId,
};
};
48 changes: 30 additions & 18 deletions frontend/app/chat/[chatId]/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
/* eslint-disable */
"use client";
import Button from "@/lib/components/ui/Button";
import { useFeature } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";

import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { useState } from "react";
import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";

import { ConfigModal } from "./components/ConfigModal";
import { MicButton } from "./components/MicButton/MicButton";
import { useChatInput } from "./hooks/useChatInput";
import { MentionItem } from "../ActionsBar/components";

export const ChatInput = (): JSX.Element => {
const [message, setMessage] = useState<string>("");
const { addQuestion, generatingAnswer, chatId } = useChat();
const { t } = useTranslation(['chat']);

const submitQuestion = () => {
if (message.length === 0) return;
if (!generatingAnswer) {
addQuestion(message, () => setMessage(""));
}
};
const { message, setMessage, submitQuestion, chatId, generatingAnswer } =
useChatInput();
const { t } = useTranslation(["chat"]);
const { currentBrain, setCurrentBrainId } = useBrainContext();
const shouldUseNewUX = useFeature("new-ux").on;

return (
<form
Expand All @@ -27,8 +24,16 @@ export const ChatInput = (): JSX.Element => {
e.preventDefault();
submitQuestion();
}}
className="sticky bottom-0 p-5 bg-white dark:bg-black rounded-t-md border border-black/10 dark:border-white/25 border-b-0 w-full max-w-3xl flex items-center justify-center gap-2 z-20"
className="sticky flex items-star bottom-0 bg-white dark:bg-black w-full flex justify-center gap-2 z-20"
>
{currentBrain !== undefined && (
<MentionItem
text={currentBrain.name}
onRemove={() => setCurrentBrainId(null)}
prefix="@"
/>
)}

<textarea
autoFocus
value={message}
Expand All @@ -40,17 +45,24 @@ export const ChatInput = (): JSX.Element => {
submitQuestion();
}
}}
className="w-full p-2 border border-gray-300 dark:border-gray-500 outline-none rounded dark:bg-gray-800"
placeholder= {t('begin_conversation_placeholder')}
className="w-full p-2 pt-0 dark:border-gray-500 outline-none rounded dark:bg-gray-800 focus:outline-none focus:border-none"
placeholder={
shouldUseNewUX
? t("actions_bar_placeholder")
: t("begin_conversation_placeholder")
}
data-testid="chat-input"
rows={1}
/>
<Button
className="px-3 py-2 sm:px-4 sm:py-2"
type="submit"
isLoading={generatingAnswer}
data-testid="submit-button"
>
{generatingAnswer ? t('thinking',{ns:'chat'}) : t('chat',{ns:'chat'})}
{generatingAnswer
? t("thinking", { ns: "chat" })
: t("chat", { ns: "chat" })}
</Button>
<div className="flex items-center">
<MicButton setMessage={setMessage} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ vi.mock("@/lib/context", () => ({
useChatContext: () => useChatContextMock(),
}));

vi.mock("@growthbook/growthbook-react", () => ({
useFeature: () => ({
on: true,
}),
}));

describe("ChatMessages", () => {
it("should render chat messages correctly", () => {
const { getAllByTestId } = render(<ChatMessages />);

expect(getAllByTestId("chat-message-speaker")).toBeDefined();
expect(getAllByTestId("brain-prompt-tags")).toBeDefined();

expect(getAllByTestId("chat-message-text")).toBeDefined();
});
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
/* eslint-disable */
"use client";
import { forwardRef, Ref } from "react";
import { useFeature } from "@growthbook/growthbook-react";
import React from "react";
import ReactMarkdown from "react-markdown";

import { cn } from "@/lib/utils";

export const ChatMessage = forwardRef(
type ChatMessageProps = {
speaker: string;
text: string;
brainName?: string;
promptName?: string;
};

export const ChatMessage = React.forwardRef(
(
{
speaker,
text,
}: {
speaker: string;
text: string;
},
ref
{ speaker, text, brainName, promptName }: ChatMessageProps,
ref: React.Ref<HTMLDivElement>
) => {
const isNewUxOn = useFeature("new-ux").on;

const isUserSpeaker = speaker === "user";
const containerClasses = cn(
"py-3 px-5 w-fit ",
isUserSpeaker
? "bg-gray-100 bg-opacity-60 items-start "
: "bg-purple-100 bg-opacity-60 items-end",
"dark:bg-gray-800 rounded-3xl flex flex-col overflow-hidden scroll-pb-32"
);

const containerWrapperClasses = cn(
"flex flex-col",

isUserSpeaker ? "items-start " : "items-end"
);

const markdownClasses = cn(
"prose",
isUserSpeaker ? "dark:prose-invert" : "dark:prose"
);

return (
<div
ref={ref as Ref<HTMLDivElement>}
className={cn(
"py-3 px-3 md:px-6 w-full dark:border-white/25 flex flex-col max-w-4xl overflow-hidden scroll-pb-32",
speaker === "user"
? ""
: "bg-gray-200 dark:bg-gray-800 bg-opacity-60 py-8"
)}
style={speaker === "user" ? { whiteSpace: "pre-line" } : {}} // Add this line to preserve line breaks
>
<span
className={cn(
"capitalize text-xs bg-sky-200 rounded-xl p-1 px-2 mb-2 w-fit dark:bg-sky-700"
<div className={containerWrapperClasses}>
{" "}
<div ref={ref} className={containerClasses}>
{isNewUxOn && (
<span
data-testid="brain-prompt-tags"
className="text-gray-400 mb-1"
>
@{brainName ?? "-"} #{promptName ?? "-"}
</span>
)}
data-testid="chat-message-speaker"
>
{speaker}
</span>
<div data-testid="chat-message-text">
<ReactMarkdown className="prose dark:prose-invert ml-[6px] mt-1">
{text}
</ReactMarkdown>
<div data-testid="chat-message-text">
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
</div>
</div>
</div>
);
Expand Down

0 comments on commit 80be40a

Please sign in to comment.