Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/src/aichat/views/ChatEventView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {forwardRef, memo} from 'react';
import {AI_CUSTOMIZATIONS_LABELS} from '@cdo/apps/aichatLab/views/modelCustomization/constants';
import AiTutorVersionActionNotification from '@cdo/apps/aiComponentLibrary/aiTutorVersionActionNotification/AiTutorVersionActionNotification';
import {useAppDispatch} from '@cdo/apps/util/reduxHooks';
import {AiChatTeacherFeedback as TeacherFeedback} from '@cdo/generated-scripts/sharedConstants';

import {modelDescriptions} from '../constants';
import {removeUpdateMessage} from '../redux';
Expand All @@ -20,6 +21,7 @@ import {
ChatEventDescriptionKey,
ChatAsset,
ModelParameters,
isCompletedChatMessage,
} from '../types';

import ChatMessageView, {getChatMessageDisplayText} from './ChatMessageView';
Expand Down Expand Up @@ -93,6 +95,10 @@ const ChatEventView = forwardRef<HTMLDivElement, ChatEventViewProps>(

// Only wrap chat messages in a focusable div for keyboard navigation
if (isChatMessage(event)) {
const teacherFlagged = isCompletedChatMessage(event)
? event.teacherFeedback === TeacherFeedback.CLEAN_DISAGREE
: false;
const teacherFlaggedHidden = teacherFlagged && !isTeacherView;
return (
Comment thread
fisher-alice marked this conversation as resolved.
<div
ref={ref}
Expand All @@ -102,17 +108,19 @@ const ChatEventView = forwardRef<HTMLDivElement, ChatEventViewProps>(
event.status,
event.role,
event.chatMessageText,
false // Profane messages are never shown in the aria-label context to prevent screen readers from reading inappropriate content.
false, // Profane messages are never shown in the aria-label context to prevent screen readers from reading inappropriate content.
teacherFlaggedHidden
)}
className={styles.chatMessageOutline}
>
<ChatMessageView
chatMessage={event}
isChatHistoryView={isTeacherView || false}
isTeacherView={isTeacherView || false}
buildAssetUrl={buildAssetUrl}
clientType={clientType}
modelParameters={modelParameters}
postText={postText}
teacherFlaggedHidden={teacherFlaggedHidden}
/>
</div>
);
Expand Down
167 changes: 114 additions & 53 deletions apps/src/aichat/views/ChatMessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,22 @@ import ProfanityFeedbackFooter from './teacherFeedback/ProfanityFeedbackFooter';
import styles from './chatWorkspace.module.scss';
interface ChatMessageViewProps {
chatMessage: ChatMessageType;
isChatHistoryView: boolean;
isTeacherView: boolean;
buildAssetUrl?: (asset: ChatAsset) => string;
clientType?: string;
modelParameters?: ModelParameters;
postText?: React.ReactNode;
teacherFlaggedHidden: boolean;
}

const ChatMessageView: React.FunctionComponent<ChatMessageViewProps> = ({
chatMessage,
isChatHistoryView,
isTeacherView,
buildAssetUrl,
clientType,
modelParameters,
postText,
teacherFlaggedHidden,
}) => {
const user = useAppSelector(state => state.currentUser);

Expand Down Expand Up @@ -72,69 +74,44 @@ const ChatMessageView: React.FunctionComponent<ChatMessageViewProps> = ({
status,
role,
intendedDisplayText,
showProfaneUserMessage
showProfaneUserMessage,
teacherFlaggedHidden
);

// If the chat message's display text is what is displayed (i.e. no error or violation)
const messageVisible =
displayText === intendedDisplayText &&
chatMessage.status !== Status.PROFANITY_VIOLATION;
chatMessage.status !== Status.PROFANITY_VIOLATION &&
!teacherFlaggedHidden;

// If a user's chat message has a profanity violation
const userMessageProfanity =
chatMessage.role === Role.USER &&
chatMessage.status === Status.PROFANITY_VIOLATION;
chatMessage.status === Status.PROFANITY_VIOLATION &&
!teacherFlaggedHidden;

const isAssistant = chatMessage.role === Role.ASSISTANT;

let footer;
if (isChatHistoryView) {
// In chat history view, all events should have been retrieved from the server (i.e. should have an ID).
if (!isServerChatEvent(chatMessage)) {
console.warn('Invalid event in chat history', chatMessage);
return null;
}

const commonProps = {
id: chatMessage.id,
chatMessageText: chatMessage.chatMessageText,
teacherFeedback: isCompletedChatMessage(chatMessage)
? chatMessage.teacherFeedback
: undefined,
};

footer = messageVisible ? (
<CleanFeedbackFooter {...commonProps} isAssistant={isAssistant} />
) : userMessageProfanity ? (
<ProfanityFeedbackFooter
{...commonProps}
toggleProfaneMessageVisibility={() =>
setShowProfaneUserMessage(!showProfaneUserMessage)
}
profaneMessageVisible={showProfaneUserMessage}
/>
) : null;
} else {
footer =
messageVisible && isAssistant ? (
<div className={styles.buttonRow}>
<CopyButton
copyText={chatMessage.chatMessageText}
usage={'ai-chat-msg-footer'}
/>
{canLogToLangfuse && isServerChatEvent(chatMessage) && (
<FlagResponseButton
chatMessageId={chatMessage.id}
chatMessageText={chatMessage.chatMessageText}
modelParameters={modelParameters}
/>
)}
</div>
) : null;
// In teacher view, all events should have been retrieved from the server (i.e. should have an ID).
if (isTeacherView && !isServerChatEvent(chatMessage)) {
console.warn('Invalid event in chat history', chatMessage);
Comment thread
fisher-alice marked this conversation as resolved.
return null;
}

const footer = getFooter({
isTeacherView,
messageVisible,
userMessageProfanity,
isAssistant,
chatMessage,
canLogToLangfuse,
modelParameters,
showProfaneUserMessage,
setShowProfaneUserMessage,
});

let header;
if (hasAssets || hasUserAddedSelectionContext) {
if ((hasAssets || hasUserAddedSelectionContext) && !teacherFlaggedHidden) {
header = (
<div
className={classNames(styles.assetCol, isAssistant && styles.assistant)}
Expand Down Expand Up @@ -187,7 +164,7 @@ const ChatMessageView: React.FunctionComponent<ChatMessageViewProps> = ({
text={displayText}
postText={postText}
role={role}
messageStyle={getMessageStyle(status, role)}
messageStyle={getMessageStyle(status, role, teacherFlaggedHidden)}
header={header}
footer={footer}
/>
Expand All @@ -198,7 +175,8 @@ export function getChatMessageDisplayText(
status: ValueOf<typeof Status>,
role: Role,
chatMessageDisplayText: string,
showProfaneUserMessage: boolean
showProfaneUserMessage: boolean,
teacherFlaggedHidden: boolean
) {
// If Role is USER, display the original message, unless there is a PII violation
// or a profanity violation and the message is not supposed to be shown.
Expand All @@ -209,10 +187,16 @@ export function getChatMessageDisplayText(
if (status === Status.PROFANITY_VIOLATION && !showProfaneUserMessage) {
return commonI18n.aiChatInappropriateUserMessage();
}
if (teacherFlaggedHidden) {
return 'This message has been flagged as inappropriate by the teacher.';
}
return chatMessageDisplayText;
Comment thread
fisher-alice marked this conversation as resolved.
}

// If Role is ASSISTANT, display the appropriate message based on the status.
if (teacherFlaggedHidden) {
return 'This message has been flagged as inappropriate by the teacher.';
}
Comment thread
fisher-alice marked this conversation as resolved.
Outdated
switch (status) {
case Status.PROFANITY_VIOLATION:
return commonI18n.aiChatInappropriateModelMessage();
Expand All @@ -231,10 +215,87 @@ export function getChatMessageDisplayText(
}
}

function getMessageStyle(status: ValueOf<typeof Status>, role: Role) {
interface GetFooterParams {
isTeacherView: boolean;
messageVisible: boolean;
userMessageProfanity: boolean;
isAssistant: boolean;
chatMessage: ChatMessageType;
canLogToLangfuse: boolean;
modelParameters: ModelParameters | undefined;
showProfaneUserMessage: boolean;
setShowProfaneUserMessage: (value: boolean) => void;
}

function getFooter({
isTeacherView,
messageVisible,
userMessageProfanity,
isAssistant,
chatMessage,
canLogToLangfuse,
modelParameters,
showProfaneUserMessage,
setShowProfaneUserMessage,
}: GetFooterParams): React.ReactNode {
if (isTeacherView) {
// Guaranteed by the component-level guard, but needed for type narrowing.
if (!isServerChatEvent(chatMessage)) return null;

const commonProps = {
id: chatMessage.id,
chatMessageText: chatMessage.chatMessageText,
teacherFeedback: isCompletedChatMessage(chatMessage)
? chatMessage.teacherFeedback
: undefined,
};

if (messageVisible) {
return <CleanFeedbackFooter {...commonProps} isAssistant={isAssistant} />;
}
if (userMessageProfanity) {
return (
<ProfanityFeedbackFooter
{...commonProps}
toggleProfaneMessageVisibility={() =>
setShowProfaneUserMessage(!showProfaneUserMessage)
}
profaneMessageVisible={showProfaneUserMessage}
/>
);
}
return null;
}

if (messageVisible && isAssistant) {
return (
<div className={styles.buttonRow}>
<CopyButton
copyText={chatMessage.chatMessageText}
usage={'ai-chat-msg-footer'}
/>
{canLogToLangfuse && isServerChatEvent(chatMessage) && (
<FlagResponseButton
chatMessageId={chatMessage.id}
chatMessageText={chatMessage.chatMessageText}
modelParameters={modelParameters}
/>
)}
</div>
);
}
return null;
}

function getMessageStyle(
status: ValueOf<typeof Status>,
role: Role,
teacherFlaggedHidden: boolean
) {
if (
status === Status.PROFANITY_VIOLATION ||
status === Status.USER_INPUT_TOO_LARGE ||
teacherFlaggedHidden ||
(role === Role.ASSISTANT &&
(status === Status.ERROR ||
status === Status.MODEL_TIMEOUT ||
Expand Down
Loading