Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
67 changes: 64 additions & 3 deletions cypress/e2e/builder/main.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const PROMPT_TEXT_AREA = '[name="Chatbot Prompt"]';
const CUE_TEXT_AREA = '[name="Conversation Starter"]';
const CHATBOT_NAME_INPUT = '[name="Chatbot Name"]';

const ADD_STARTER_SUGGESTION_BUTTON = '[title="Add starter suggestion"]';
const buildStarterSuggestionInput = (idx: number) =>
`[name="Starter suggestion number ${idx}"]`;

describe('Builder View', () => {
it('Results table', () => {
cy.setUpApi(
Expand Down Expand Up @@ -46,7 +50,7 @@ describe('Builder View', () => {
// show default values
cy.get(buildDataCy(CHATBOT_SETTINGS_SUMMARY_CY))
.should('contain', 'Graasp Bot')
.should('contain', 'The conversation starter is empty');
.should('contain', '-');

cy.get(EDIT_SETTINGS_BUTTON).click();

Expand All @@ -64,7 +68,6 @@ describe('Builder View', () => {
cy.get(buildDataCy(CHATBOT_SETTINGS_SUMMARY_CY))
.should('contain', 'Graasp Bot')
.should('contain', prompt)
.should('not.contain', 'The conversation starter is empty')
.should('contain', cue);
});

Expand Down Expand Up @@ -106,7 +109,65 @@ describe('Builder View', () => {
cy.get(buildDataCy(CHATBOT_SETTINGS_SUMMARY_CY))
.should('contain', name)
.should('contain', prompt)
.should('not.contain', 'The conversation starter is empty')
.should('contain', cue);
});

it('Show starter suggestions and edit', () => {
cy.setUpApi(
{ appSettings: [] },
{
context: Context.Builder,
permission: PermissionLevel.Admin,
},
);
cy.visit('/');

cy.get(buildDataCy(TAB_SETTINGS_VIEW_CYPRESS)).click();

cy.get(EDIT_SETTINGS_BUTTON).click();

// change prompt
const prompt = 'my new prompt';
cy.get(PROMPT_TEXT_AREA).clear().type(prompt);

const newSuggestions = ['Hello', 'Hello1', 'Hello2'];

// add suggestions
newSuggestions.forEach((s, idx) => {
cy.get(ADD_STARTER_SUGGESTION_BUTTON).click();
cy.get(buildStarterSuggestionInput(idx)).type(s);
});

cy.get(SAVE_SETTINGS_BUTTON).click();

// show starter suggestions
for (const s of newSuggestions) {
cy.get(`li:contains('${s}')`).should('be.visible');
}

cy.wait(1000);
cy.get(EDIT_SETTINGS_BUTTON).click();

// edit suggestions
const editedSuggestions = ['newHello0', 'newHello1', 'newHello2'];
editedSuggestions.forEach((s, idx) => {
cy.get(buildStarterSuggestionInput(idx)).clear().type(s);
});

// delete second suggestion
cy.get(`[title="Delete suggestion number 1"]`).click();

// expect remaining suggestions
const remainingSuggestions = [editedSuggestions[0], editedSuggestions[2]];
for (const s of remainingSuggestions) {
cy.get(`[value="${s}"]`).should('be.visible');
}

cy.get(SAVE_SETTINGS_BUTTON).click();

// show starter suggestions
for (const s of remainingSuggestions) {
cy.get(`li:contains('${s}')`).should('be.visible');
}
});
});
35 changes: 34 additions & 1 deletion cypress/e2e/player/main.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('Player View', () => {
cy.visit('/');

// expect previously saved app data
const previousAppData = defaultAppData[0];
const [previousAppData] = defaultAppData;
cy.get(buildDataCy(buildCommentContainerDataCy(previousAppData.id))).should(
'contain',
previousAppData.data.content,
Expand Down Expand Up @@ -138,4 +138,37 @@ describe('Player View', () => {
cy.get('#root').should('contain', 'November 18, 2024');
cy.get('#root').should('contain', 'November 18, 2025');
});

it('Use a starter suggestion', () => {
cy.setUpApi(
{
appData: [],
appSettings: [MOCK_APP_SETTING],
},
{
context: Context.Player,
permission: PermissionLevel.Write,
},
);
cy.visit('/');

const [suggestion] = MOCK_APP_SETTING.data.starterSuggestions;

// click suggestion
cy.get(`button:contains("${suggestion}")`).click();

// expect user message
cy.get(buildDataCy(buildCommentContainerDataCy('2'))).should(
'contain',
suggestion,
);

// expect chatbot message
cy.get(buildDataCy(buildCommentContainerDataCy('3'))).should(
'contain',
'i am a bot', // default return value of the mocked chatbot
);

cy.get('button').should('not.contain', suggestion);
});
});
1 change: 1 addition & 0 deletions cypress/fixtures/mockSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const MOCK_APP_SETTING = {
chatbotCue: 'cue',
chatbotName: 'name',
initialPrompt: 'prompt',
starterSuggestions: ['Can you spell it for me?', 'Can you explain this?'],
},
};
1 change: 1 addition & 0 deletions src/config/appActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const AppActionsType = {
Reply: 'reply_comment',
// chatbot actions
AskChatbot: 'ask_chatbot',
UseStarter: 'use_starter',
} as const;
2 changes: 2 additions & 0 deletions src/config/appSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ export const ChatbotPromptSettingsKeys = {
InitialPrompt: 'initialPrompt',
ChatbotCue: 'chatbotCue',
ChatbotName: 'chatbotName',
StarterSuggestions: 'starterSuggestions',
} as const;

export type ChatbotPromptSettings = {
[ChatbotPromptSettingsKeys.InitialPrompt]: string;
[ChatbotPromptSettingsKeys.ChatbotCue]: string;
[ChatbotPromptSettingsKeys.ChatbotName]: string;
[ChatbotPromptSettingsKeys.StarterSuggestions]: string[];
// used to allow access using settings[settingKey] syntax
// [key: string]: unknown;
};
Expand Down
14 changes: 12 additions & 2 deletions src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@
"CHATBOT_PROMPT_HELPER_LABEL": "In-depth technical description",
"CHATBOT_PROMPT_HELPER": "This defines the chatbot's personality and how it should behave.",
"CHATBOT_PROMPT_FORMAT_HELPER": "To describe the initial situation, create an object at the start of the array with 2 keys: 'role' and 'content'. For the initial description the role must be 'system'. Place your description of the initial situation in the 'content' key. Add interaction examples after that. Add one object per message with the role corresponding to either 'assistant' or 'user'.",
"CHATBOT_STARTER_SUGGESTIONS_LABEL": "Starter Suggestions",
"CHATBOT_STARTER_SUGGESTIONS_EMPTY_MESSAGE": "-",
"CHATBOT_PROMPT_FORMAT_EXAMPLE": "Here is an example",
"CHATBOT_PROMPT_API_REFERENCE": "See the API reference",
"CHATBOT_CUE_LABEL": "Conversation Starter",
"CHATBOT_STARTER_SUGGESTION_LABEL": "Starter Suggestions",
"CHATBOT_CUE_HELPER": "This defines the content of the first message of the chatbot. You can change it to better orient the conversation.",
"CHATBOT_CUE_EMPTY_MESSAGE": "The conversation starter is empty.",
"CHATBOT_CUE_EMPTY_MESSAGE": "-",
"SAVE_LABEL": "Save",
"SAVED_LABEL": "Saved",
"GENERAL_SETTING_TITLE": "General",
Expand Down Expand Up @@ -64,6 +67,13 @@
"CONFIGURE_BUTTON": "Configure",
"CLOSE": "Close",
"SIGN_OUT_ALERT": "You should be signed in to interact with the chatbot",
"ANALYTICS_CONVERSATION_MEMBER": "Conversation"
"ANALYTICS_CONVERSATION_MEMBER": "Conversation",
"CHATBOT_STARTER_SUGGESTION_HELPER": "This defines suggested messages the user can send at the beginning of the conversation to the chatbot. You can change it to give ideas of interactions.",
"ADD_STARTER_SUGGESTION_BUTTON": "Add",
"ADD_STARTER_SUGGESTION_BUTTON_TITLE": "Add starter suggestion",
"STARTER_SUGGESTION_NAME": "Starter suggestion number {{nb}}",
"DELETE_STARTER_SUGGESTION_BUTTON_TITLE": "Delete suggestion number {{nb}}",
"STARTER_SUGGESTION_PLACEHOLDER": "Write a suggestion here",
"STARTER_SUGGESTION_PLAYER_BUTTON_ARIA_LABEL": "Ask {{suggestion}}"
}
}
2 changes: 1 addition & 1 deletion src/modules/analytics/FrequentWords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function FrequentWords({
>
<DialogTitle>{t('ANALYTICS_CONVERSATION_MEMBER')}</DialogTitle>
<DialogContent>
<ConversationForUser userId={chatMemberID} />
<ConversationForUser accountId={chatMemberID} />
</DialogContent>
<DialogActions>
<Button>{t('CLOSE')}</Button>
Expand Down
9 changes: 9 additions & 0 deletions src/modules/comment/Conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ChatbotPromptSettings } from '@/config/appSetting';

import CommentEditor from '../common/CommentEditor';
import CommentThread from '../common/CommentThread';
import { Suggestions } from '../common/Suggestions';
import { Comment } from '../common/useConversation';
import ChatbotHeader from './ChatbotHeader';
import CommentContainer from './CommentContainer';
Expand All @@ -25,13 +26,15 @@ function Conversation({
chatbotAvatar,
isLoading,
mode = 'read',
suggestions,
}: Readonly<{
chatbotPrompt?: ChatbotPromptSettings;
chatbotAvatar?: Blob;
threadSx?: SxProps<Theme>;
isLoading?: boolean;
comments: Comment[];
mode?: 'read' | 'write';
suggestions?: string[];
}>) {
const { t } = useTranslation();

Expand All @@ -56,6 +59,12 @@ function Conversation({
threadSx={threadSx}
comments={comments}
/>
{suggestions && (
<Suggestions
suggestions={suggestions}
chatbotPrompt={chatbotPrompt}
/>
)}
{'write' === mode && (
<CommentEditor chatbotPrompt={chatbotPrompt} />
)}
Expand Down
9 changes: 5 additions & 4 deletions src/modules/comment/ConversationForUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { useConversation } from '../common/useConversation';
import Conversation from './Conversation';

export const ConversationForUser = ({
userId,
}: Readonly<{ userId: string }>) => {
const { comments, isLoading, chatbotPrompt, chatbotAvatar } =
useConversation(userId);
accountId,
}: Readonly<{ accountId: string }>) => {
const { comments, isLoading, chatbotPrompt, chatbotAvatar } = useConversation(
{ accountId },
);

return (
<Conversation
Expand Down
2 changes: 1 addition & 1 deletion src/modules/common/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function BotComment({
pr={10}
>
<Stack>
<ChatbotAvatar size="small" avatar={avatar} />;
<ChatbotAvatar size="small" avatar={avatar} />
</Stack>
<Stack sx={{ py: 0 }} alignItems="start" gap={1}>
<Typography variant="subtitle2">{name}</Typography>
Expand Down
35 changes: 8 additions & 27 deletions src/modules/common/CommentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import {
} from '@/config/selectors';
import { SMALL_BORDER_RADIUS } from '@/constants';

import { useAskChatbot } from './useAskChatbot';
import { useSendMessage } from './useSendMessage';
import { useSendMessageAndAskChatbot } from './useSendMessageAndAskChatbot';

const TextArea = styled(TextareaAutosize)(({ theme }) => ({
borderRadius: SMALL_BORDER_RADIUS,
Expand Down Expand Up @@ -59,28 +58,10 @@ function CommentEditor({
const [text, setText] = useState('');
const [textTooLong, setTextTooLong] = useState('');

const { generateChatbotAnswer, isLoading: askChatbotLoading } =
useAskChatbot(chatbotPrompt);
const { sendMessage, isLoading: sendMessageLoading } =
useSendMessage(chatbotPrompt);

const onSendHandler = async (newUserComment: string) => {
if (!chatbotPrompt) {
throw new Error(
"unexpected error, chatbot setting is not present, can't sent to API without it",
);
}

try {
const userMessage = await sendMessage(newUserComment);

await generateChatbotAnswer(userMessage.id, newUserComment);

setText('');
} catch (e) {
console.error(e);
}
};
const { send, isLoading } = useSendMessageAndAskChatbot({
chatbotPrompt,
onSend: () => setText(''),
});

const handleTextChange = ({
target: { value },
Expand All @@ -106,7 +87,7 @@ function CommentEditor({
value={text}
onChange={handleTextChange}
role="textbox"
disabled={sendMessageLoading || askChatbotLoading}
disabled={isLoading}
// use default font instead of textarea's monospace font
style={{ fontFamily: 'unset' }}
/>
Expand All @@ -118,8 +99,8 @@ function CommentEditor({
data-cy={COMMENT_EDITOR_SAVE_BUTTON_CYPRESS}
color="primary"
variant="contained"
onClick={() => onSendHandler(text)}
loading={askChatbotLoading || sendMessageLoading}
onClick={() => send(text)}
loading={isLoading}
name="send"
>
{t('SEND_LABEL')}
Expand Down
60 changes: 60 additions & 0 deletions src/modules/common/Suggestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useTranslation } from 'react-i18next';

import { Button, Stack, styled } from '@mui/material';

import { AppActionsType } from '@/config/appActions';
import { ChatbotPromptSettings } from '@/config/appSetting';
import { mutations } from '@/config/queryClient';
import { showErrorToast } from '@/utils/toast';

import { useSendMessageAndAskChatbot } from './useSendMessageAndAskChatbot';

const StyledButton = styled(Button)({
borderRadius: 100,
textTransform: 'unset',
});

export function Suggestions({
suggestions,
chatbotPrompt,
}: Readonly<{
chatbotPrompt: ChatbotPromptSettings;
suggestions: string[];
}>) {
const { t } = useTranslation();
const { mutateAsync: postAction } = mutations.usePostAppAction();
const { send } = useSendMessageAndAskChatbot({ chatbotPrompt });

const onClick = async (suggestion: string) => {
try {
await send(suggestion);
await postAction({
type: AppActionsType.UseStarter,
data: { value: suggestion },
});
} catch (e: unknown) {
if (e instanceof Error) {
showErrorToast(e.message);
}
console.error(e);
}
};

return (
<Stack direction="row" gap={2} justifyContent="right">
{suggestions.map((s) => (
<span key={s}>
<StyledButton
aria-label={t('STARTER_SUGGESTION_PLAYER_BUTTON_ARIA_LABEL', {
suggestion: s,
})}
onClick={() => onClick(s)}
variant="contained"
>
{s}
</StyledButton>
</span>
))}
</Stack>
);
}
Loading