diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6810fb37..c96bf28b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,5 +20,6 @@ jobs: - name: Build run: yarn build - - name: Unit Tests - run: yarn vitest + # enable again when we have unit tests + # - name: Unit Tests + # run: yarn vitest diff --git a/cypress/e2e/analytics/main.cy.ts b/cypress/e2e/analytics/main.cy.ts index b35c22a3..fd3dcdd3 100644 --- a/cypress/e2e/analytics/main.cy.ts +++ b/cypress/e2e/analytics/main.cy.ts @@ -7,7 +7,6 @@ import { ANALYTICS_VIEW_CY, ANALYTICS_WORDS_CLOUD_MODAL_ID, KEYWORD_CHIP_COUNT_ID, - PLAYER_VIEW_CY, buildCheckWholeMemberChatButtonId, buildDataCy, buildKeywordChipId, @@ -72,7 +71,14 @@ describe('Analytics View', () => { cy.get( `#${buildCheckWholeMemberChatButtonId(actions[0]?.account?.id)}`, ).click(); - cy.get(buildDataCy(PLAYER_VIEW_CY)).should('be.visible'); + + // show conversation + cy.get('[role="dialog"]') + .should('contain', MOCK_APP_SETTING.data.chatbotCue) + .should('contain', MOCK_APP_SETTING.data.chatbotName); + + // do not show textbox + cy.get('[role="textbox"]').should('not.exist'); }); }); }); diff --git a/cypress/e2e/player/main.cy.ts b/cypress/e2e/player/main.cy.ts index b99a60fb..514b6fdf 100644 --- a/cypress/e2e/player/main.cy.ts +++ b/cypress/e2e/player/main.cy.ts @@ -1,7 +1,6 @@ import { AppDataVisibility, Context, PermissionLevel } from '@graasp/sdk'; import { - PLAYER_VIEW_CY, buildCommentContainerDataCy, buildDataCy, } from '../../../src/config/selectors'; @@ -24,7 +23,7 @@ const defaultAppData = [ ]; describe('Player View', () => { - beforeEach(() => { + it('Show messages and write a new one', () => { cy.setUpApi( { appData: defaultAppData, @@ -36,10 +35,6 @@ describe('Player View', () => { }, ); cy.visit('/'); - }); - - it('Show messages and write a new one', () => { - cy.get(buildDataCy(PLAYER_VIEW_CY)).should('be.visible'); // expect previously saved app data const previousAppData = defaultAppData[0]; @@ -65,4 +60,82 @@ describe('Player View', () => { 'i am a bot', // default return value of the mocked chatbot ); }); + + it('Show cue and write a message', () => { + cy.setUpApi( + { + appData: [], + appSettings: [MOCK_APP_SETTING], + }, + { + context: Context.Player, + permission: PermissionLevel.Write, + }, + ); + cy.visit('/'); + + // expect cue + cy.get(buildDataCy(buildCommentContainerDataCy('cue'))).should( + 'contain', + MOCK_APP_SETTING.data.chatbotCue, + ); + + // type and send message + const message = 'My message'; + cy.get('[role="textbox"]').type(message); + cy.get('[name="send"]').click(); + + // expect user message + cy.get(buildDataCy(buildCommentContainerDataCy('2'))).should( + 'contain', + message, + ); + + // expect chatbot message + cy.get(buildDataCy(buildCommentContainerDataCy('3'))).should( + 'contain', + 'i am a bot', // default return value of the mocked chatbot + ); + }); + + it('Show dates', () => { + cy.setUpApi( + { + appData: [ + { + account: CURRENT_MEMBER, + createdAt: '2025-11-18T16:35:22.010Z', + creator: CURRENT_MEMBER, + data: { content: 'A previously saved message' }, + id: '0', + item: MOCK_SERVER_ITEM, + type: 'comment', + updatedAt: '2025-11-18T16:35:22.010Z', + visibility: AppDataVisibility.Member, + }, + { + account: CURRENT_MEMBER, + createdAt: '2024-11-18T16:35:22.010Z', + creator: CURRENT_MEMBER, + data: { content: 'A previously saved message' }, + id: '1', + item: MOCK_SERVER_ITEM, + type: 'comment', + updatedAt: '2025-11-18T16:35:22.010Z', + visibility: AppDataVisibility.Member, + }, + ], + appSettings: [MOCK_APP_SETTING], + }, + { + context: Context.Player, + permission: PermissionLevel.Write, + }, + ); + cy.visit('/'); + + // expect dates + cy.get('#root').should('contain', 'November 18, 2024'); + cy.get('#root').should('contain', 'November 18, 2025'); + }); }); diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 9d3439ef..29014044 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -3,7 +3,6 @@ export const TABLE_VIEW_TABLE_CYPRESS = 'table_view_table'; export const TABLE_VIEW_PANE_CYPRESS = 'table_view_pane'; export const SETTINGS_VIEW_PANE_CYPRESS = 'settings_view_pane'; export const ABOUT_VIEW_PANE_CYPRESS = 'about_view_pane'; -export const PLAYER_VIEW_CY = 'player-view'; export const BUILDER_VIEW_CY = 'builder-view'; export const ANALYTICS_VIEW_CY = 'analytics_view'; export const TAB_PRESET_VIEW_CYPRESS = 'tab_preset_view'; @@ -102,7 +101,6 @@ export const buildChatbotPromptContainerDataCy = (id: string): string => export const buildCommentResponseBoxDataCy = (id: string): string => `${COMMENT_RESPONSE_BOX_CY}-${id}`; -export const COMMENT_THREAD_CONTAINER_CYPRESS = 'comment_thread_container'; export const ORPHAN_BUTTON_CYPRESS = 'orphan_button'; export const CODE_EXECUTION_CONTAINER_CYPRESS = 'code_execution_container'; export const CODE_EDITOR_ID_CY = 'code_editor'; diff --git a/src/langs/en.json b/src/langs/en.json index 8987ebc0..2cce0721 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -61,6 +61,9 @@ "FILTER_BY_COMMON_KEYWORDS": "Filter by most frequent keywords", "SEARCH_BY_OTHER_KEYWORDS": "Search by other keywords or regex", "CHECK_WHOLE_CHAT": "Check the whole chat", - "CONFIGURE_BUTTON": "Configure" + "CONFIGURE_BUTTON": "Configure", + "CLOSE": "Close", + "SIGN_OUT_ALERT": "You should be signed in to interact with the chatbot", + "ANALYTICS_CONVERSATION_MEMBER": "Conversation" } } diff --git a/src/modules/analytics/FrequentWords.tsx b/src/modules/analytics/FrequentWords.tsx index 66ed3aa6..074b030a 100644 --- a/src/modules/analytics/FrequentWords.tsx +++ b/src/modules/analytics/FrequentWords.tsx @@ -4,7 +4,10 @@ import { useTranslation } from 'react-i18next'; import { Button, Chip, - Grid2, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Stack, TextField, Typography, @@ -19,9 +22,9 @@ import { buildKeywordChipId, } from '@/config/selectors'; +import { ConversationForUser } from '../comment/ConversationForUser'; import KeywordChip from '../common/KeywordChip'; import TextWithHighlightedKeywords from '../common/TextWithHighlightedKeywords'; -import PlayerView from '../main/PlayerView'; import { createRegexFromString, getTopFrequentWords } from './utils'; type Props = { @@ -126,43 +129,41 @@ function FrequentWords({ ))} - - - - {commentsMatchSelectedWords.map((ele) => ( - setChatMemberID(ele.account.id)} - buttonId={buildCheckWholeMemberChatButtonId(ele.account.id)} - /> - ))} + + {commentsMatchSelectedWords.map((ele) => ( + setChatMemberID(ele.account.id)} + buttonId={buildCheckWholeMemberChatButtonId(ele.account.id)} + /> + ))} - { - // oxlint-disable-next-line eslint/yoda - commentsMatchSelectedWords.length === 0 && ( - {t('NO_RESULTS_MATCH_WORDS')} - ) - } - - - {chatMemberID && ( - - - - )} - + { + // oxlint-disable-next-line eslint/yoda + commentsMatchSelectedWords.length === 0 && ( + {t('NO_RESULTS_MATCH_WORDS')} + ) + } + + {chatMemberID && ( + { + setChatMemberID(''); + }} + > + {t('ANALYTICS_CONVERSATION_MEMBER')} + + + + + + + + )} ); } diff --git a/src/modules/comment/ChatbotHeader.tsx b/src/modules/comment/ChatbotHeader.tsx new file mode 100644 index 00000000..570901c3 --- /dev/null +++ b/src/modules/comment/ChatbotHeader.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { Stack, Typography } from '@mui/material'; + +import ChatbotAvatar from '../common/ChatbotAvatar'; + +function ChatbotHeader({ name }: Readonly<{ name: string }>) { + return ( + + + + {name} + + + ); +} + +export default ChatbotHeader; diff --git a/src/modules/comment/CommentContainer.tsx b/src/modules/comment/CommentContainer.tsx index 27f3e928..9fefec0c 100644 --- a/src/modules/comment/CommentContainer.tsx +++ b/src/modules/comment/CommentContainer.tsx @@ -5,7 +5,7 @@ import { BIG_BORDER_RADIUS } from '../../constants'; const CommentContainer = styled('div')(({ theme }) => ({ backgroundColor: 'white', border: 'solid silver 1px', - padding: theme.spacing(1, 0), + padding: theme.spacing(3, 0), borderRadius: BIG_BORDER_RADIUS, })); export default CommentContainer; diff --git a/src/modules/comment/Conversation.tsx b/src/modules/comment/Conversation.tsx new file mode 100644 index 00000000..aa690d5e --- /dev/null +++ b/src/modules/comment/Conversation.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next'; + +import { + Alert, + Box, + CircularProgress, + Divider, + Stack, + SxProps, + Theme, +} from '@mui/material'; + +import { ChatbotPromptSettings } from '@/config/appSetting'; + +import CommentEditor from '../common/CommentEditor'; +import CommentThread from '../common/CommentThread'; +import { Comment } from '../common/useConversation'; +import ChatbotHeader from './ChatbotHeader'; +import CommentContainer from './CommentContainer'; + +function Conversation({ + threadSx, + comments, + chatbotPrompt, + isLoading, + mode = 'read', +}: Readonly<{ + chatbotPrompt?: ChatbotPromptSettings; + threadSx?: SxProps; + isLoading?: boolean; + comments: Comment[]; + mode?: 'read' | 'write'; +}>) { + const { t } = useTranslation(); + + if (chatbotPrompt) { + const { chatbotName } = chatbotPrompt; + + return ( + + + + + + + {'write' === mode && ( + + )} + + + + ); + } + + if (isLoading) { + return ; + } + + return {t('CHATBOT_CONFIGURATION_MISSING')}; +} + +export default Conversation; diff --git a/src/modules/comment/ConversationForUser.tsx b/src/modules/comment/ConversationForUser.tsx new file mode 100644 index 00000000..6dc67b46 --- /dev/null +++ b/src/modules/comment/ConversationForUser.tsx @@ -0,0 +1,17 @@ +import { useConversation } from '../common/useConversation'; +import Conversation from './Conversation'; + +export const ConversationForUser = ({ + userId, +}: Readonly<{ userId: string }>) => { + const { comments, isLoading, chatbotPrompt } = useConversation(userId); + + return ( + + ); +}; diff --git a/src/modules/common/ChatbotPrompt.tsx b/src/modules/common/ChatbotPrompt.tsx index 204d222b..617fd5f6 100644 --- a/src/modules/common/ChatbotPrompt.tsx +++ b/src/modules/common/ChatbotPrompt.tsx @@ -1,83 +1,18 @@ -import { useTranslation } from 'react-i18next'; - -import { Alert, CardContent, CardHeader } from '@mui/material'; - -import { useLocalContext } from '@graasp/apps-query-client'; -import type { UUID } from '@graasp/sdk'; - -import type { CommentData } from '@/config/appData'; -import type { ChatbotPromptSettings } from '@/config/appSetting'; -import { SettingsKeys } from '@/config/appSetting'; -import { hooks } from '@/config/queryClient'; -import { buildChatbotPromptContainerDataCy } from '@/config/selectors'; import { DEFAULT_BOT_USERNAME } from '@/constants'; -import CustomCommentCard from '../comment/CustomCommentCard'; -import ChatbotAvatar from './ChatbotAvatar'; -import CommentBody from './CommentBody'; +import { Comment } from './Comment'; type Props = { - id?: UUID; + chatbotCue?: string; + chatbotName?: string; }; -function ChatbotPrompt({ id }: Props): JSX.Element | null { - const { t } = useTranslation(); - const { data: appData } = hooks.useAppData(); - const { - data: chatbotPrompts, - isSuccess, - isError, - } = hooks.useAppSettings({ - name: SettingsKeys.ChatbotPrompt, - }); - const chatbotPrompt = chatbotPrompts?.[0]; - - const { accountId } = useLocalContext(); - - const comments = appData?.filter((c) => c.creator?.id === (id ?? accountId)); - - const realChatbotPromptExists = comments?.find( - (c) => c.data.chatbotPromptSettingId !== undefined, - ); - - if (!chatbotPrompt) { - if (isSuccess) { - return ( - {t('CHATBOT_CONFIGURATION_MISSING')} - ); - } - if (isError) { - return ( - {t('CHATBOT_CONFIGURATION_FETCH_ERROR')} - ); - } - // do not show anything if it has not finished fetching - return null; // oxlint-disable-line eslint-plugin-unicorn/no-null - } - const chatbotName = chatbotPrompt?.data?.chatbotName || DEFAULT_BOT_USERNAME; - - // display only if real chatbot prompt does not exist yet - if (!realChatbotPromptExists) { - if ('' === chatbotPrompt?.data?.chatbotCue) { - return <>Please configure the chatbot prompt.; - } - return ( - <> - - } - /> - - {chatbotPrompt?.data?.chatbotCue} - - - - ); +function ChatbotPrompt({ + chatbotCue, + chatbotName = DEFAULT_BOT_USERNAME, +}: Readonly): JSX.Element | null { + if (chatbotCue) { + return ; } return null; } diff --git a/src/modules/common/Comment.tsx b/src/modules/common/Comment.tsx index 58f052db..4a73b6c4 100644 --- a/src/modules/common/Comment.tsx +++ b/src/modules/common/Comment.tsx @@ -1,69 +1,52 @@ import { useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { CardProps } from '@mui/material'; -import { Card, CardContent, CardHeader, styled } from '@mui/material'; +import { Stack, Typography } from '@mui/material'; -import { useLocalContext } from '@graasp/apps-query-client'; -import { formatDate } from '@graasp/sdk'; - -import type { CommentAppData } from '@/config/appData'; -import { AppDataTypes } from '@/config/appData'; -import type { ChatbotPromptSettings } from '@/config/appSetting'; -import { ChatbotPromptSettingsKeys, SettingsKeys } from '@/config/appSetting'; -import { hooks } from '@/config/queryClient'; import { buildCommentContainerDataCy } from '@/config/selectors'; -import { BIG_BORDER_RADIUS, DEFAULT_BOT_USERNAME } from '@/constants'; import ChatbotAvatar from './ChatbotAvatar'; import CommentBody from './CommentBody'; import CustomAvatar from './CustomAvatar'; -const CustomCard = styled(Card)({ - borderRadius: BIG_BORDER_RADIUS, -}); - type Props = { - comment: CommentAppData; + id: string; + body: string; + isBot: boolean; + username: string; }; -function Comment({ comment }: Props): JSX.Element { - const { i18n } = useTranslation(); - - const { accountId } = useLocalContext(); - const { data: appContext } = hooks.useAppContext(); - const currentMember = appContext?.members.find((m) => m.id === accountId); - const { data: chatbotPrompts } = hooks.useAppSettings({ - name: SettingsKeys.ChatbotPrompt, - }); - const chatbotPrompt = chatbotPrompts?.[0]; +export function Comment({ + id, + isBot, + body, + username, +}: Readonly): JSX.Element { const commentRef = useRef(null); - const isBot = comment.type === AppDataTypes.BotComment; + const avatar = isBot ? ( + + ) : ( + + ); return ( - - : - } - /> - - {comment.data.content} - - + + + {avatar} + + {username} + + {body} + + + + ); } - -export default Comment; diff --git a/src/modules/common/CommentBody.tsx b/src/modules/common/CommentBody.tsx index 70436c3d..ec324e13 100644 --- a/src/modules/common/CommentBody.tsx +++ b/src/modules/common/CommentBody.tsx @@ -11,6 +11,8 @@ import { BIG_BORDER_RADIUS } from '@/constants'; // oxlint-disable no-magic-numbers const StyledReactMarkdown = styled(ReactMarkdown)(({ theme }) => ({ + borderRadius: 8, + padding: theme.spacing(1), '& .prism-code': { fontFamily: 'var(--monospace-fonts)', backgroundColor: 'transparent !important', @@ -112,11 +114,17 @@ function code(props: { ); } -function CommentBody({ children }: Readonly<{ children: string }>) { +function CommentBody({ + children, + background = '#efefef', +}: Readonly<{ children: string; background?: string }>) { return ( {children} diff --git a/src/modules/common/CommentEditor.tsx b/src/modules/common/CommentEditor.tsx index 6a5f238c..5ae6deb9 100644 --- a/src/modules/common/CommentEditor.tsx +++ b/src/modules/common/CommentEditor.tsx @@ -61,7 +61,8 @@ function CommentEditor({ const { generateChatbotAnswer, isLoading: askChatbotLoading } = useAskChatbot(chatbotPrompt); - const { sendMessage, isLoading: sendMessageLoading } = useSendMessage(); + const { sendMessage, isLoading: sendMessageLoading } = + useSendMessage(chatbotPrompt); const onSendHandler = async (newUserComment: string) => { if (!chatbotPrompt) { diff --git a/src/modules/common/CommentThread.tsx b/src/modules/common/CommentThread.tsx index 98ed4141..84ae5a82 100644 --- a/src/modules/common/CommentThread.tsx +++ b/src/modules/common/CommentThread.tsx @@ -1,35 +1,73 @@ -import { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Fragment } from 'react/jsx-runtime'; import type { SxProps, Theme } from '@mui/material'; -import { Box } from '@mui/material'; +import { Stack, Typography } from '@mui/material'; -import type { CommentAppData } from '@/config/appData'; -import { COMMENT_THREAD_CONTAINER_CYPRESS } from '@/config/selectors'; +import { intlFormat } from 'date-fns'; +import groupby from 'lodash.groupby'; -import Comment from './Comment'; +import { Comment } from './Comment'; +import { type Comment as CommentType } from './useConversation'; type Props = { - comments?: CommentAppData[]; - threadSx: SxProps; + comments?: CommentType[]; + threadSx?: SxProps; }; function CommentThread({ comments, threadSx, }: Readonly): JSX.Element | null { + const { i18n } = useTranslation(); + //oxlint-disable-next-line eslint/yoda if (!comments || comments.length === 0) { return null; } + const commentsPerDay = groupby(comments, (c) => + intlFormat( + new Date(c.createdAt), + { + year: 'numeric', + month: 'long', + day: 'numeric', + }, + { locale: i18n.language }, + ), + ); + return ( - - {comments.map((c) => ( - - - - ))} - + + {Object.entries(commentsPerDay).map(([date, commentsForDay]) => { + return ( + + + {date} + + + {commentsForDay.map((c) => { + return ( + + ); + })} + + + ); + })} + ); } diff --git a/src/modules/common/CustomAvatar.tsx b/src/modules/common/CustomAvatar.tsx index 862b5895..d08bae98 100644 --- a/src/modules/common/CustomAvatar.tsx +++ b/src/modules/common/CustomAvatar.tsx @@ -1,25 +1,29 @@ import { Avatar } from '@mui/material'; -import type { Member } from '@graasp/sdk'; import { stringToColor } from '@graasp/ui/apps'; import { ANONYMOUS_USER } from '@/constants'; -import { getInitials } from '@/utils/utils'; type Props = { - member?: Member; + username?: string; imgSrc?: string; }; -function CustomAvatar({ member, imgSrc }: Readonly): JSX.Element { - const userName = member?.name ?? ANONYMOUS_USER; +function CustomAvatar({ + username = ANONYMOUS_USER, + imgSrc, +}: Readonly): JSX.Element { return ( - {getInitials(userName)} + {username[0]} ); } diff --git a/src/modules/common/CustomDialog.tsx b/src/modules/common/CustomDialog.tsx index 22d5e515..f3ac1304 100644 --- a/src/modules/common/CustomDialog.tsx +++ b/src/modules/common/CustomDialog.tsx @@ -1,4 +1,9 @@ -import type { MutableRefObject, ReactElement, RefObject } from 'react'; +import type { + MutableRefObject, + ReactElement, + ReactNode, + RefObject, +} from 'react'; import type { Breakpoint } from '@mui/material'; import { @@ -38,7 +43,7 @@ const StyledDialogTitle = styled(DialogTitle)({}); type Props = { open: boolean; title: string | ReactElement; - content: ReactElement | string; + children: ReactNode; actions?: ReactElement; onClose?: () => void; dataCy?: string; @@ -52,7 +57,6 @@ type Props = { function CustomDialog({ open, title, - content, actions, onClose, dataCy, @@ -61,6 +65,7 @@ function CustomDialog({ maxWidth = 'sm', noPadding = false, anchor = null, + children, }: Props) { return ( - {content} + {children} {actions} diff --git a/src/modules/common/useConversation.tsx b/src/modules/common/useConversation.tsx new file mode 100644 index 00000000..bdd7d68a --- /dev/null +++ b/src/modules/common/useConversation.tsx @@ -0,0 +1,61 @@ +import { AppDataTypes, CommentData } from '@/config/appData'; +import { ChatbotPromptSettings, SettingsKeys } from '@/config/appSetting'; +import { hooks } from '@/config/queryClient'; +import { ANONYMOUS_USER, DEFAULT_BOT_USERNAME } from '@/constants'; + +export type Comment = { + id: string; + body: string; + createdAt: string; + isBot: boolean; + username: string; +}; + +export const useConversation = (accountId?: string) => { + const { data: appData, isLoading: isAppDataLoading } = + hooks.useAppData(); + const { data: chatbotPromptSettings, isLoading: isChatbotSettingsLoading } = + hooks.useAppSettings({ + name: SettingsKeys.ChatbotPrompt, + }); + + const chatbotPrompt = chatbotPromptSettings?.[0]?.data; + + // get comments for given user only + const comments = + appData + ?.filter((res) => res.creator?.id === accountId) + ?.toSorted((c1, c2) => (c1.createdAt > c2.createdAt ? 1 : -1)) + ?.map((c) => { + const isBot = c.type === AppDataTypes.BotComment; + return { + id: c.id, + createdAt: c.createdAt, + isBot, + body: c.data.content, + username: isBot + ? (chatbotPrompt?.chatbotName ?? DEFAULT_BOT_USERNAME) + : (c.account.name ?? ANONYMOUS_USER), + }; + }) ?? []; + + // include cue as comment if there is no comments + const chatbotCueComment = + chatbotPrompt?.chatbotCue && 0 === comments.length + ? [ + { + id: 'cue', + isBot: true, + createdAt: new Date().toISOString(), + body: chatbotPrompt.chatbotCue, + username: DEFAULT_BOT_USERNAME, + }, + ] + : []; + + return { + comments: [...chatbotCueComment, ...comments], + chatbotPrompt, + isLoading: isAppDataLoading || isChatbotSettingsLoading, + }; +}; diff --git a/src/modules/common/useSendMessage.tsx b/src/modules/common/useSendMessage.tsx index 6509798b..f8bd637d 100644 --- a/src/modules/common/useSendMessage.tsx +++ b/src/modules/common/useSendMessage.tsx @@ -1,21 +1,31 @@ import { useCallback } from 'react'; import { AppActionsType } from '@/config/appActions'; -import { AppDataTypes } from '@/config/appData'; -import { mutations } from '@/config/queryClient'; +import { AppDataTypes, CommentData } from '@/config/appData'; +import { hooks, mutations } from '@/config/queryClient'; /** * Create a function that save an app data with the user message * @returns sendMessage function */ -export const useSendMessage = () => { +export const useSendMessage = ({ chatbotCue }: { chatbotCue?: string }) => { + const { data: comments } = hooks.useAppData(); const { mutateAsync: postAppDataAsync, isLoading } = mutations.usePostAppData(); const { mutateAsync: postAppActionAsync } = mutations.usePostAppAction(); const sendMessage = useCallback( async (newUserComment: string) => { - // post new user comment as appData with normal call + // save cue on first comment + if (chatbotCue && 0 === comments?.length) { + await postAppDataAsync({ + data: { + content: chatbotCue, + }, + type: AppDataTypes.BotComment, + }); + } + const userMessage = await postAppDataAsync({ data: { content: newUserComment, @@ -30,7 +40,7 @@ export const useSendMessage = () => { return userMessage; }, - [postAppDataAsync, postAppActionAsync], + [comments?.length, chatbotCue, postAppDataAsync, postAppActionAsync], ); return { sendMessage, isLoading }; diff --git a/src/modules/main/AnalyticsView.tsx b/src/modules/main/AnalyticsView.tsx index 44cd9ec1..69e00e2b 100644 --- a/src/modules/main/AnalyticsView.tsx +++ b/src/modules/main/AnalyticsView.tsx @@ -51,7 +51,7 @@ function AnalyticsView(): JSX.Element { marginTop={1} justifyContent="center" > - + } title={t('STATISTIC_TOTAL_USER_COMMENTS_TITLE')} @@ -65,7 +65,7 @@ function AnalyticsView(): JSX.Element { - + } title={t('STATISTIC_AVERAGE_USER_COMMENTS_TITLE')} @@ -76,7 +76,7 @@ function AnalyticsView(): JSX.Element { - + } title={t('WORDS_FREQUENCY')} @@ -92,7 +92,6 @@ function AnalyticsView(): JSX.Element { - ( - <> - - - - - - ); - return ( @@ -150,9 +136,17 @@ function ConversationsView() { open={openCommentView} maxWidth="lg" title={t('DISCUSSION_DIALOG_TITLE', { user: currentUser.name })} - content={renderDialogContent()} onClose={onCloseDialog} - /> + > + + + + + ); } diff --git a/src/modules/main/PlayerView.tsx b/src/modules/main/PlayerView.tsx index bd75f266..4a56b95d 100644 --- a/src/modules/main/PlayerView.tsx +++ b/src/modules/main/PlayerView.tsx @@ -1,68 +1,28 @@ import { useTranslation } from 'react-i18next'; -import type { SxProps, Theme } from '@mui/material'; -import { Alert, Box, CircularProgress } from '@mui/material'; +import { Typography } from '@mui/material'; import { useLocalContext } from '@graasp/apps-query-client'; -import type { UUID } from '@graasp/sdk'; -import type { CommentData } from '@/config/appData'; -import { ChatbotPromptSettings, SettingsKeys } from '@/config/appSetting'; -import { hooks } from '@/config/queryClient'; -import { PLAYER_VIEW_CY } from '@/config/selectors'; -import ChatbotPrompt from '@/modules/common/ChatbotPrompt'; -import CommentThread from '@/modules/common/CommentThread'; +import Conversation from '../comment/Conversation'; +import { useConversation } from '../common/useConversation'; -import CommentContainer from '../comment/CommentContainer'; -import CommentEditor from '../common/CommentEditor'; - -type Props = { - id?: UUID; - threadSx?: SxProps; -}; - -function PlayerView({ id, threadSx = {} }: Readonly): JSX.Element { +function PlayerView(): JSX.Element { const { t } = useTranslation(); - const { data: appData, isLoading: isAppDataLoading } = - hooks.useAppData(); - const { data: chatbotPromptSettings, isLoading: isChatbotSettingsLoading } = - hooks.useAppSettings({ - name: SettingsKeys.ChatbotPrompt, - }); - - let { accountId } = useLocalContext(); - if (id) { - accountId = id; - } - - if (chatbotPromptSettings && 0 < chatbotPromptSettings.length && appData) { - const comments = appData - .filter((res) => res.creator?.id === accountId) - .toSorted((c1, c2) => (c1.createdAt > c2.createdAt ? 1 : -1)); - - return ( - - - - - - - - ); - } + const { accountId } = useLocalContext(); + const { chatbotPrompt, comments, isLoading } = useConversation(accountId); - if (isAppDataLoading || isChatbotSettingsLoading) { - return ; + if (!accountId) { + return {t('SIGN_OUT_ALERT')}; } - return {t('CHATBOT_CONFIGURATION_MISSING')}; + return ( + + ); } export default PlayerView; diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts deleted file mode 100644 index 4d56c2b5..00000000 --- a/src/utils/utils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { getInitials } from './utils'; - -describe('initials util', () => { - it('simple 2 part name', () => { - expect(getInitials('Bob Doe')).toBe('BD'); - }); - it('single part name', () => { - expect(getInitials('Bob')).toBe('B'); - }); - it('name with hyphen', () => { - expect(getInitials('Bob-Doe')).toBe('BD'); - }); - it('special char at beginning of second name', () => { - expect(getInitials('Bob (Doe)')).toBe('BD'); - }); - it('3 part name', () => { - expect(getInitials('Bob doe the Great')).toBe('BdtG'); - }); - it('only special symbols', () => { - expect(getInitials(String.raw`~/\%`)).toBe(''); - }); -}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts deleted file mode 100644 index 9896e75b..00000000 --- a/src/utils/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getInitials = (name: string): string => - name - .split(/[^a-z]/i) - .map((c) => Array.from(c).filter((l) => l.match(/[a-z]/i))[0]) - .join('');