Skip to content

Commit

Permalink
test: add stories to chat messages
Browse files Browse the repository at this point in the history
  • Loading branch information
codeincontext committed Nov 28, 2024
1 parent df29cdd commit fdabf7e
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 144 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ChatMessagePart } from "./ChatMessagePart";

const meta: Meta<typeof ChatMessagePart> = {
title: "Components/Chat/ChatMessagePart",
component: ChatMessagePart,
tags: ["autodocs"],
args: {
inspect: false,
},
};

export default meta;
type Story = StoryObj<typeof ChatMessagePart>;

export const PromptMessagePart: Story = {
args: {
part: {
document: {
type: "prompt",
message:
"Are the learning outcome and learning cycles appropriate for your pupils? If not, suggest an edit. Otherwise, tap **Continue** to move on to the next step.",
},
},
},
};

export const ErrorMessagePart: Story = {
args: {
part: {
document: {
type: "error",
message:
"**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in 11 hours. If you require a higher limit, please [make a request](https://forms.gle/tHsYMZJR367zydsG8).",
},
},
},
};

export const TextMessagePart: Story = {
args: {
part: {
document: {
type: "text",
value:
"Are the learning outcome and learning cycles appropriate for your pupils? If not, suggest an edit. Otherwise, tap **Continue** to move on to the next step.",
},
},
},
};

export const WithInspector: Story = {
args: {
inspect: true,
part: {
document: {
type: "prompt",
message: "This is a prompt",
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type {
ErrorDocument,
MessagePart,
PromptDocument,
TextDocument,
} from "@oakai/aila/src/protocol/jsonPatchProtocol";
import { aiLogger } from "@oakai/logger";

import { MemoizedReactMarkdownWithStyles } from "@/components/AppComponents/Chat/markdown";

import type { ModerationModalHelpers } from "../../FeedbackForms/ModerationFeedbackModal";

const log = aiLogger("chat");

const components = {
comment: NonRenderedPart,
prompt: PromptMessagePart,
error: ErrorMessagePart,
bad: NonRenderedPart,
patch: NonRenderedPart,
/**
* Patches do not get rendered, they get applied to the lesson plan
* state, which is then rendered in the right hand side.
*/
experimentalPatch: NonRenderedPart,
state: NonRenderedPart,
text: TextMessagePart,
action: NonRenderedPart,
moderation: NonRenderedPart,
id: NonRenderedPart,
unknown: NonRenderedPart,
};

export interface ChatMessagePartProps {
part: MessagePart;
inspect: boolean;
moderationModalHelpers: ModerationModalHelpers;
}

export function ChatMessagePart({
part,
inspect,
}: Readonly<ChatMessagePartProps>) {
const PartComponent = components[part.document.type] as React.ComponentType<{
part: typeof part.document;
}>;

if (!PartComponent) {
log.info("Unknown part type", part.document.type, JSON.stringify(part));
return null;
}

return (
<div className="w-full">
<PartComponent part={part.document} />

{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inspect && <PartInspector part={part} />
}
</div>
);
}

function NonRenderedPart() {
return null;
}

function PromptMessagePart({ part }: Readonly<{ part: PromptDocument }>) {
return <MemoizedReactMarkdownWithStyles markdown={part.message} />;
}

function ErrorMessagePart({
part,
}: Readonly<{
part: ErrorDocument;
}>) {
const markdown = part.message ?? "Sorry, an error has occurred";
return <MemoizedReactMarkdownWithStyles markdown={markdown} />;
}

function TextMessagePart({ part }: Readonly<{ part: TextDocument }>) {
return <MemoizedReactMarkdownWithStyles markdown={part.value} />;
}

function PartInspector({ part }: Readonly<{ part: MessagePart }>) {
return (
<div className="w-full bg-gray-200 px-8 py-16">
<pre className="w-full text-wrap text-xs">
{JSON.stringify(part, null, 2)}
</pre>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ChatModerationProvider } from "@/components/ContextProviders/ChatModerationContext";

import { ChatMessage } from "./";

const meta: Meta<typeof ChatMessage> = {
title: "Components/Chat/ChatMessage",
component: ChatMessage,
tags: ["autodocs"],
decorators: [
(Story) => (
<ChatModerationProvider chatId="test-chat-id">
<Story />
</ChatModerationProvider>
),
],
args: {
persistedModerations: [],
},
};

export default meta;
type Story = StoryObj<typeof ChatMessage>;

export const UserMessage: Story = {
args: {
message: {
id: "test-chat-id",
content:
"Create a lesson plan about the end of Roman Britain for key stage 3 history",
role: "user",
},
},
};

export const LlmMessage: Story = {
args: {
message: {
id: "test-chat-id",
content:
'{"type":"llmMessage","sectionsToEdit":["learningOutcome","learningCycles"],"patches":[{"type":"patch","reasoning":"Since there are no existing Oak lessons for this topic, I have created a new lesson plan from scratch focusing on the end of Roman Britain.","value":{"type":"string","op":"add","path":"/learningOutcome","value":"I can explain the reasons behind the decline of Roman Britain and its impact on society."},"status":"complete"},{"type":"patch","reasoning":"I have outlined the learning cycles to break down the lesson structure for teaching about the end of Roman Britain.","value":{"type":"string-array","op":"add","path":"/learningCycles","value":["Identify the key events leading to the end of Roman Britain.","Describe the societal changes that occurred post-Roman withdrawal.","Analyse the archaeological evidence of Roman Britain\'s legacy."]},"status":"complete"}],"sectionsEdited":["learningOutcome","learningCycles"],"prompt":{"type":"text","value":"Are the learning outcome and learning cycles appropriate for your pupils? If not, suggest an edit. Otherwise, tap **Continue** to move on to the next step."},"status":"complete"}',
role: "assistant",
},
},
};

export const ErrorMessage: Story = {
args: {
message: {
id: "test-chat-id",
role: "assistant",
content:
'{"type":"error","value":"Rate limit exceeded","message":"**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in 11 hours. If you require a higher limit, please [make a request](https://forms.gle/tHsYMZJR367zydsG8)."}',
},
},
};
146 changes: 2 additions & 144 deletions apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,21 @@
import type { ReactNode } from "react";
import { useState } from "react";

import type {
ActionDocument,
BadDocument,
CommentDocument,
ErrorDocument,
MessagePart,
ModerationDocument,
PatchDocument,
PromptDocument,
StateDocument,
TextDocument,
UnknownDocument,
} from "@oakai/aila/src/protocol/jsonPatchProtocol";
import type { MessagePart } from "@oakai/aila/src/protocol/jsonPatchProtocol";
import { parseMessageParts } from "@oakai/aila/src/protocol/jsonPatchProtocol";
import { isSafe } from "@oakai/core/src/utils/ailaModeration/helpers";
import type { PersistedModerationBase } from "@oakai/core/src/utils/ailaModeration/moderationSchema";
import { aiLogger } from "@oakai/logger";
import type { Message } from "ai";

import { MemoizedReactMarkdownWithStyles } from "@/components/AppComponents/Chat/markdown";
import { useChatModeration } from "@/components/ContextProviders/ChatModerationContext";
import { Icon } from "@/components/Icon";
import { cn } from "@/lib/utils";

import type { ModerationModalHelpers } from "../../FeedbackForms/ModerationFeedbackModal";
import type { AilaStreamingStatus } from "../Chat/hooks/useAilaStreamingStatus";
import { ChatMessagePart } from "./ChatMessagePart";
import { isModeration } from "./protocol";

const log = aiLogger("chat");

export interface ChatMessageProps {
chatId: string; // Needed for when we refactor to use a moderation provider
message: Message;
Expand Down Expand Up @@ -220,130 +205,3 @@ function MessageTextWrapper({ children }: Readonly<{ children: ReactNode }>) {
</div>
);
}

export interface ChatMessagePartProps {
part: MessagePart;
inspect: boolean;
moderationModalHelpers: ModerationModalHelpers;
}

function ChatMessagePart({
part,
inspect,
moderationModalHelpers,
}: Readonly<ChatMessagePartProps>) {
const PartComponent = {
comment: CommentMessagePart,
prompt: PromptMessagePart,
error: ErrorMessagePart,
bad: BadMessagePart,
patch: PatchMessagePart,
experimentalPatch: ExperimentalPatchMessageComponent,
state: StateMessagePart,
text: TextMessagePart,
action: ActionMessagePart,
moderation: ModerationMessagePart,
id: IdMessagePart,
unknown: UnknownMessagePart,
}[part.document.type] as React.ComponentType<{
part: typeof part.document;
moderationModalHelpers: ModerationModalHelpers;
}>;

if (!PartComponent) {
log.info("Unknown part type", part.document.type, JSON.stringify(part));
return null;
}

return (
<div className="w-full">
<PartComponent
part={part.document}
moderationModalHelpers={moderationModalHelpers}
/>

{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inspect && <PartInspector part={part} />
}
</div>
);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function BadMessagePart({ part }: Readonly<{ part: BadDocument }>) {
return null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function CommentMessagePart({ part }: Readonly<{ part: CommentDocument }>) {
return null;
}

function PromptMessagePart({ part }: Readonly<{ part: PromptDocument }>) {
return <MemoizedReactMarkdownWithStyles markdown={part.message} />;
}

function ModerationMessagePart({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
part,
}: Readonly<{ part: ModerationDocument }>) {
return null;
}

function ErrorMessagePart({
part,
}: Readonly<{
part: ErrorDocument;
}>) {
const markdown = part.message ?? "Sorry, an error has occurred";
return <MemoizedReactMarkdownWithStyles markdown={markdown} />;
}

function TextMessagePart({ part }: Readonly<{ part: TextDocument }>) {
return <MemoizedReactMarkdownWithStyles markdown={part.value} />;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function PatchMessagePart({ part }: Readonly<{ part: PatchDocument }>) {
return null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function StateMessagePart({ part }: Readonly<{ part: StateDocument }>) {
return null;
}

function IdMessagePart() {
return null;
}

function ActionMessagePart({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
part,
}: Readonly<{ part: ActionDocument }>) {
return null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function UnknownMessagePart({ part }: Readonly<{ part: UnknownDocument }>) {
return null;
}

function PartInspector({ part }: Readonly<{ part: MessagePart }>) {
return (
<div className="w-full bg-gray-200 px-8 py-16">
<pre className="w-full text-wrap text-xs">
{JSON.stringify(part, null, 2)}
</pre>
</div>
);
}

/**
* Patches do not get rendered, they get applied to the lesson plan
* state, which is then rendered in the right hand side.
*/
function ExperimentalPatchMessageComponent() {
return null;
}

0 comments on commit fdabf7e

Please sign in to comment.