From 5ecb603d872c994c9b3d6652736787abc098b0e0 Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:17:17 +0100 Subject: [PATCH] test: add chat panel stories --- .../Chat/chat-left-hand-side.tsx | 7 +- .../AppComponents/Chat/chat-panel.stories.tsx | 178 ++++++++++++++++++ .../AppComponents/Chat/chat-panel.tsx | 19 +- .../AppComponents/Chat/prompt-form.tsx | 25 +-- .../analytics/lessonPlanTrackingContext.tsx | 2 +- apps/nextjs/src/lib/hooks/use-sidebar.tsx | 4 +- 6 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx index ba3fab32e..321b4ad2e 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Flex } from "@radix-ui/themes"; +import { ButtonScrollToBottom } from "@/components/AppComponents/Chat/button-scroll-to-bottom"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import type { DemoContextProps } from "@/components/ContextProviders/Demo"; @@ -52,10 +53,8 @@ const ChatLeftHandSide = ({ )} - + + ); diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx new file mode 100644 index 000000000..c2cff638c --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + ChatContext, + type ChatContextProps, +} from "@/components/ContextProviders/ChatProvider"; +import { lessonPlanTrackingContext } from "@/lib/analytics/lessonPlanTrackingContext"; +import { SidebarContext } from "@/lib/hooks/use-sidebar"; + +import { ChatPanel } from "./chat-panel"; + +const DummyMessage = {}; + +const ChatDecorator: Story["decorators"] = (Story, { parameters }) => ( + + + +); + +const LessonPlanTrackingContextDecorator: Story["decorators"] = (Story) => ( + {}, + onClickRetry: () => {}, + onClickStartFromExample: () => {}, + onClickStartFromFreeText: () => {}, + onStreamFinished: () => {}, + onSubmitText: () => {}, + }} + > + + +); + +const SidebarContextDecorator: Story["decorators"] = (Story) => ( + {}, + isLoading: false, + isSidebarOpen: false, + }} + > + + +); + +const meta: Meta = { + title: "Components/Chat/ChatPanel", + component: ChatPanel, + tags: ["autodocs"], + decorators: [ + ChatDecorator, + LessonPlanTrackingContextDecorator, + SidebarContextDecorator, + ], + args: { + isDemoLocked: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoMessages: Story = { + args: {}, + parameters: { + chatContext: { + messages: [], + }, + }, +}; + +export const DemoLocked: Story = { + args: { + isDemoLocked: true, + }, +}; + +export const Idle: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Idle", + }, + }, +}; + +export const IdleWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "Idle", + }, + }, +}; + +export const Loading: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Loading", + }, + }, +}; + +export const RequestMade: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "RequestMade", + }, + }, +}; + +export const StreamingLessonPlan: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const StreamingChatResponse: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingChatResponse", + }, + }, +}; + +export const StreamingWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const Moderating: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Moderating", + }, + }, +}; + +export const ModeratingWithRegenerateUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "Moderating", + }, + }, +}; + +export const CustomQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "Increase the reading age of that section", + ailaStreamingStatus: "Moderating", + }, + }, +}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx index fca32ed91..867d85fa4 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx @@ -1,6 +1,5 @@ import { cva } from "class-variance-authority"; -import { ButtonScrollToBottom } from "@/components/AppComponents/Chat/button-scroll-to-bottom"; import { PromptForm } from "@/components/AppComponents/Chat/prompt-form"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import useAnalytics from "@/lib/analytics/useAnalytics"; @@ -8,7 +7,6 @@ import useAnalytics from "@/lib/analytics/useAnalytics"; import ChatPanelDisclaimer from "./chat-panel-disclaimer"; interface ChatPanelProps { - isEmptyScreen: boolean; isDemoLocked: boolean; } @@ -18,14 +16,12 @@ function LockedPromptForm() { ); } -export function ChatPanel({ - isEmptyScreen, - isDemoLocked, -}: Readonly) { +export function ChatPanel({ isDemoLocked }: Readonly) { const chat = useLessonChat(); const { id, isLoading, + messages, input, setInput, append, @@ -34,12 +30,13 @@ export function ChatPanel({ queuedUserAction, } = chat; + const hasMessages = !!messages.length; + const { trackEvent } = useAnalytics(); - const containerClass = `grid w-full grid-cols-1 ${isEmptyScreen ? "sm:grid-cols-1" : ""} peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]`; + const containerClass = `grid w-full grid-cols-1 ${hasMessages ? "sm:grid-cols-1" : ""} peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]`; return (
- -
+
{!isDemoLocked && ( { @@ -57,7 +54,7 @@ export function ChatPanel({ input={input} setInput={setInput} ailaStreamingStatus={ailaStreamingStatus} - isEmptyScreen={isEmptyScreen} + hasMessages={hasMessages} queueUserAction={queueUserAction} queuedUserAction={queuedUserAction} /> @@ -73,7 +70,7 @@ export function ChatPanel({ const chatBoxWrap = cva(["mx-auto w-full "], { variants: { - isEmptyScreen: { + hasMessages: { false: "max-w-2xl ", true: "", }, diff --git a/apps/nextjs/src/components/AppComponents/Chat/prompt-form.tsx b/apps/nextjs/src/components/AppComponents/Chat/prompt-form.tsx index 04e600d87..b2369b0ca 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/prompt-form.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/prompt-form.tsx @@ -17,8 +17,7 @@ import type { AilaStreamingStatus } from "./Chat/hooks/useAilaStreamingStatus"; export interface PromptFormProps extends Pick { onSubmit: (value: string) => void; - isEmptyScreen: boolean; - placeholder?: string; + hasMessages: boolean; ailaStreamingStatus: AilaStreamingStatus; queuedUserAction?: string | null; queueUserAction?: (action: string) => void; @@ -29,8 +28,7 @@ export function PromptForm({ onSubmit, input, setInput, - isEmptyScreen, - placeholder, + hasMessages, queuedUserAction, queueUserAction, }: Readonly) { @@ -93,8 +91,8 @@ export function PromptForm({ value={input} onChange={(e) => setInput(e.target.value)} placeholder={handlePlaceholder( - isEmptyScreen, - queuedUserAction ?? placeholder, + hasMessages, + queuedUserAction ?? undefined, )} spellCheck={false} className="min-h-[60px] w-full resize-none bg-transparent px-10 py-[1.3rem] text-base focus-within:outline-none" @@ -119,11 +117,14 @@ export function PromptForm({ ); } -function handlePlaceholder(isEmptyScreen: boolean, placeholder?: string) { - if (placeholder && !["continue", "regenerate"].includes(placeholder)) { - return placeholder; +function handlePlaceholder(hasMessages: boolean, queuedUserAction?: string) { + if ( + queuedUserAction && + !["continue", "regenerate"].includes(queuedUserAction) + ) { + return queuedUserAction; } - return !isEmptyScreen - ? "Type a subject, key stage and title" - : "Type your response here"; + return hasMessages + ? "Type your response here" + : "Type a subject, key stage and title"; } diff --git a/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx b/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx index 2f9e9a3af..d664640d4 100644 --- a/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx +++ b/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx @@ -30,7 +30,7 @@ type LessonPlanTrackingContext = { onClickStartFromFreeText: (text: string) => void; }; -const lessonPlanTrackingContext = +export const lessonPlanTrackingContext = createContext(null); export type LessonPlanTrackingProviderProps = Readonly<{ diff --git a/apps/nextjs/src/lib/hooks/use-sidebar.tsx b/apps/nextjs/src/lib/hooks/use-sidebar.tsx index b95c386ad..eaba67064 100644 --- a/apps/nextjs/src/lib/hooks/use-sidebar.tsx +++ b/apps/nextjs/src/lib/hooks/use-sidebar.tsx @@ -17,7 +17,9 @@ export interface SidebarContext { isLoading: boolean; } -const SidebarContext = createContext(undefined); +export const SidebarContext = createContext( + undefined, +); export function useSidebar() { const context = useContext(SidebarContext);