Skip to content

Commit

Permalink
feat: add additional materials button - AI-539 [migration] (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
JBR90 authored Nov 12, 2024
1 parent 854950d commit d0fe2d0
Show file tree
Hide file tree
Showing 21 changed files with 948 additions and 173 deletions.
2 changes: 1 addition & 1 deletion apps/nextjs/src/app/api/chat/errorHandling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe("handleChatException", () => {
type: "error",
value: "Rate limit exceeded",
message:
"**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in 1 hour. If you require a higher limit, please [make a request](https://forms.gle/tHsYMZJR367zydsG8).",
"**Unfortunately you’ve exceeded your fair usage limit for today.** Please come back in 1 hour. If you require a higher limit, please [make a request](https://share.hsforms.com/118hyngR-QSS0J7vZEVlRSgbvumd).",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useRef, useState } from "react";

import { getLastAssistantMessage } from "@oakai/aila/src/helpers/chat/getLastAssistantMessage";
import { OakBox } from "@oaknational/oak-components";
import type { AilaUserModificationAction } from "@prisma/client";

import { useLessonChat } from "@/components/ContextProviders/ChatProvider";
import { trpc } from "@/utils/trpc";

import ActionButton from "./action-button";
import type {
AdditionalMaterialOptions,
ModifyOptions,
} from "./action-button.types";
import { ActionDropDown } from "./action-drop-down";
import type { FeedbackOption } from "./drop-down-form-wrapper";

type ActionButtonWrapperProps = {
sectionTitle: string;
sectionPath: string;
sectionValue: Record<string, unknown> | string | Array<unknown>;
options: ModifyOptions | AdditionalMaterialOptions;
buttonText: string;
actionButtonLabel: string;
userSuggestionTitle: string;
tooltip: string;
generateMessage: (
option: FeedbackOption<AilaUserModificationAction>,
userFeedbackText: string,
) => string;
};

const ActionButtonWrapper = ({
sectionTitle,
sectionPath,
sectionValue,
options,
actionButtonLabel,
tooltip,
buttonText,
userSuggestionTitle,
generateMessage,
}: ActionButtonWrapperProps) => {
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [userFeedbackText, setUserFeedbackText] = useState("");
const [selectedRadio, setSelectedRadio] =
useState<FeedbackOption<AilaUserModificationAction> | null>(null);

const chat = useLessonChat();
const { append, id, messages } = chat;
const { mutateAsync } = trpc.chat.chatFeedback.modifySection.useMutation();

const lastAssistantMessage = getLastAssistantMessage(messages);

const recordUserModifySectionContent = async () => {
if (selectedRadio && lastAssistantMessage) {
const payload = {
chatId: id,
messageId: lastAssistantMessage.id,
sectionPath,
sectionValue,
action: selectedRadio.enumValue,
actionOtherText: userFeedbackText || null,
};
await mutateAsync(payload);
}
};

const handleSubmit = async () => {
if (!selectedRadio) return;
const message = generateMessage(selectedRadio, userFeedbackText);
await Promise.all([
append({ content: message, role: "user" }),
recordUserModifySectionContent(),
]);
setIsOpen(false);
};

return (
<OakBox $position="relative" ref={dropdownRef}>
<ActionButton onClick={() => setIsOpen(!isOpen)} tooltip={tooltip}>
{actionButtonLabel}
</ActionButton>

{isOpen && (
<ActionDropDown
sectionTitle={sectionTitle}
options={options}
selectedRadio={selectedRadio}
setSelectedRadio={setSelectedRadio}
isOpen={isOpen}
setIsOpen={setIsOpen}
setUserFeedbackText={setUserFeedbackText}
handleSubmit={handleSubmit}
buttonText={buttonText}
userSuggestionTitle={userSuggestionTitle}
dropdownRef={dropdownRef}
id={id}
/>
)}
</OakBox>
);
};

export default ActionButtonWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export const additionalMaterialsModifyOptions = [
{
label: "A homework task",
enumValue: "ADD_HOMEWORK_TASK",
chatMessage: "Add a homework task",
},
{
label: "A narrative for my explanation",
enumValue: "ADD_NARRATIVE",
chatMessage: "Add a narrative for my explanation",
},
{
label: "Additional practice questions",
enumValue: "ADD_PRACTICE_QUESTIONS",
chatMessage: "Add additional practice questions",
},
{
label: "Practical instructions (if relevant)",
enumValue: "ADD_PRACTICAL_INSTRUCTIONS",
chatMessage: "Add practical instructions",
},
{ label: "Other", enumValue: "OTHER" },
] as const;

export type AdditionalMaterialOptions = typeof additionalMaterialsModifyOptions;

export const modifyOptions = [
{
label: "Make it easier",
enumValue: "MAKE_IT_EASIER",
chatMessage: "easier",
},
{
label: "Make it harder",
enumValue: "MAKE_IT_HARDER",
chatMessage: "harder",
},
{
label: "Shorten content",
enumValue: "SHORTEN_CONTENT",
chatMessage: "shorter",
},
{
label: "Add more detail",
enumValue: "ADD_MORE_DETAIL",
chatMessage: "more detailed",
},
{ label: "Other", enumValue: "OTHER" },
] as const;

export type ModifyOptions = typeof modifyOptions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Dispatch, RefObject, SetStateAction } from "react";

import { aiLogger } from "@oakai/logger";
import { OakP, OakRadioGroup } from "@oaknational/oak-components";
import { $Enums, AilaUserModificationAction } from "@prisma/client";
import { TextArea } from "@radix-ui/themes";

import {
AdditionalMaterialOptions,
ModifyOptions,
} from "./action-button.types";
import { DropDownFormWrapper, FeedbackOption } from "./drop-down-form-wrapper";
import { SmallRadioButton } from "./small-radio-button";

const log = aiLogger("chat");

type DropDownProps = {
sectionTitle: string;
options: ModifyOptions | AdditionalMaterialOptions;
selectedRadio: FeedbackOption<AilaUserModificationAction> | null;
setSelectedRadio: Dispatch<
SetStateAction<FeedbackOption<$Enums.AilaUserModificationAction> | null>
>;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
setUserFeedbackText: (text: string) => void;
handleSubmit: (
option: FeedbackOption<AilaUserModificationAction>,
) => Promise<void>;
buttonText: string;
userSuggestionTitle: string;
dropdownRef: RefObject<HTMLDivElement>;
id: string;
};

export const ActionDropDown = ({
sectionTitle,
options,
selectedRadio,
setSelectedRadio,
isOpen,
setIsOpen,
setUserFeedbackText,
handleSubmit,
buttonText,
userSuggestionTitle,
dropdownRef,
id,
}: DropDownProps) => {
return (
<DropDownFormWrapper
onClickActions={handleSubmit}
setIsOpen={setIsOpen}
selectedRadio={selectedRadio}
title={sectionTitle}
buttonText={buttonText}
isOpen={isOpen}
dropdownRef={dropdownRef}
>
<OakRadioGroup
name={`drop-down-${options[0].enumValue}`}
$flexDirection="column"
$gap="space-between-s"
$background="white"
>
{options.map((option) => {
return (
<SmallRadioButton
id={`${id}-modify-options-${option.enumValue}`}
key={`${id}-modify-options-${option.enumValue}`}
value={option.enumValue}
label={handleLabelText({
text: option.label,
section: sectionTitle,
})}
onClick={() => {
setSelectedRadio(option);
}}
/>
);
})}

{selectedRadio?.label === "Other" && (
<>
<OakP $font="body-3">{userSuggestionTitle}</OakP>
<TextArea
data-testid={"modify-other-text-area"}
onChange={(e) => setUserFeedbackText(e.target.value)}
/>
</>
)}
</OakRadioGroup>
</DropDownFormWrapper>
);
};

function handleLabelText({
text,
section,
}: {
text: string;
section: string;
}): string {
log.info("section", section);
if (
section === "Misconceptions" ||
section === "Key learning points" ||
section === "Learning cycles" ||
"additional materials"
) {
if (text.split(" ").includes("it")) {
return text.replace("it", "them");
}
}
return text;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { AilaUserModificationAction } from "@prisma/client";

import ActionButtonWrapper from "./action-button-wrapper";
import { additionalMaterialsModifyOptions } from "./action-button.types";
import type { FeedbackOption } from "./drop-down-form-wrapper";

export type AdditionalMaterialsProps = {
sectionTitle: string;
sectionPath: string;
sectionValue: Record<string, unknown> | string | Array<unknown>;
};

const AddAdditionalMaterialsButton = ({
sectionTitle,
sectionPath,
sectionValue,
}: AdditionalMaterialsProps) => {
const generateMessage = (
option: FeedbackOption<AilaUserModificationAction>,
userFeedbackText: string,
) =>
option.label === "Other"
? `For the ${sectionTitle}, ${userFeedbackText}`
: `${option.chatMessage} to the additional materials section`;

return (
<ActionButtonWrapper
sectionTitle={`Ask Aila to add:`}
sectionPath={sectionPath}
sectionValue={sectionValue}
options={additionalMaterialsModifyOptions}
actionButtonLabel="Add additional materials"
tooltip="Aila can help add additional materials"
userSuggestionTitle="What additional materials would you like to add?"
generateMessage={generateMessage}
buttonText={"Add materials"}
/>
);
};

export default AddAdditionalMaterialsButton;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitles

import { sectionTitle } from ".";
import { MemoizedReactMarkdownWithStyles } from "../markdown";
import AddAdditionalMaterialsButton from "./add-additional-materials-button";
import FlagButton from "./flag-button";
import ModifyButton from "./modify-button";

Expand All @@ -28,11 +29,20 @@ const ChatSection = ({
$position="relative"
$display={["none", "flex"]}
>
<ModifyButton
sectionTitle={sectionTitle(objectKey)}
sectionPath={objectKey}
sectionValue={value}
/>
{objectKey === "additionalMaterials" && value === "None" ? (
<AddAdditionalMaterialsButton
sectionTitle={sectionTitle(objectKey)}
sectionPath={objectKey}
sectionValue={value}
/>
) : (
<ModifyButton
sectionTitle={sectionTitle(objectKey)}
sectionPath={objectKey}
sectionValue={value}
/>
)}

<FlagButton
sectionTitle={sectionTitle(objectKey)}
sectionPath={objectKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const DropDownFormWrapper = <
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
}, [dropdownRef, isOpen, setIsOpen]);

return (
<form
Expand Down
Loading

0 comments on commit d0fe2d0

Please sign in to comment.