From 1313a181e70239d2307c584e353a375938b4b83c Mon Sep 17 00:00:00 2001 From: Juan Carlos Farah Date: Wed, 4 Sep 2024 11:34:53 +0200 Subject: [PATCH 1/2] feat: add export functionality (#4) * feat: add option in chat settings to send complete message history to chatbot * feat: add export functionality for exporting conversations * feat: add interaction and exchange names * feat: export id instead of name for debugging purposes --------- Co-authored-by: deRohrer --- src/config/appSettings.ts | 3 + src/langs/en.json | 9 +- src/langs/fr.json | 9 +- src/modules/context/SettingsContext.tsx | 3 + .../interaction/ParticipantInteraction.tsx | 1 + src/modules/message/MessagesPane.tsx | 12 +- src/results/ConversationsView.tsx | 224 ++++++++---------- src/settings/ChatSettings.tsx | 26 +- src/settings/ExchangesSettings.tsx | 19 +- src/types/Message.ts | 3 +- 10 files changed, 175 insertions(+), 134 deletions(-) diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 34576bb..a2248d3 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -9,13 +9,16 @@ export type AssistantsSettingsType = { }; export type ChatSettingsType = { + name: string; description: string; participantInstructions: string; participantEndText: string; + sendAllToChatbot: boolean; }; export type ExchangeSettings = { id: UUID; + name: string; assistant: AssistantSettings; description: string; chatbotInstructions: string; diff --git a/src/langs/en.json b/src/langs/en.json index eb20e9c..aee7795 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -32,13 +32,16 @@ }, "CHAT": { "TITLE": "Chat Settings", + "NAME": "Interaction Name", "DESCRIPTION": "Chat Description", "INSTRUCTIONS": "Participant Instructions", "END": "End Screen Text", - "USER": "Participant" + "SEND_ALL": "Let Assistant Remember All Messages", + "SEND_ALL_INFO": "If checked, all previous messages will be included in the prompt sent to the chatbot, instead of only messages from the current exchange." }, "EXCHANGES": { "TITLE": "Exchanges Settings", + "NAME": "Exchange Name", "DESCRIPTION": "Exchanges Description", "INSTRUCTIONS": "Chatbot instructions", "CUE": "Initial Cue", @@ -54,6 +57,7 @@ }, "CONVERSATIONS": { "TITLE": "View Conversations", + "EXPORT_ALL": "Export All", "TABLE": { "MEMBER": "Member", "UPDATED": "Last change", @@ -61,7 +65,8 @@ "NOT_STARTED": "Not started", "COMPLETE": "Completed", "INCOMPLETE": "Not completed", - "DELETE": "Delete interaction", + "DELETE": "Delete", + "EXPORT": "Export", "NONE": "No conversations so far." }, "RESET": "Delete and reset conversation" diff --git a/src/langs/fr.json b/src/langs/fr.json index 22c4ef4..41c6a07 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -32,13 +32,16 @@ }, "CHAT": { "TITLE": " Paramètres du Chat", + "NAME": "Nom de l'Interaction", "DESCRIPTION": "Description du Chat", "INSTRUCTIONS": "Instructions pour le Participant", "END": "Texte de Fin d'Écran", - "USER": "Participant" + "SEND_ALL": "Laissez l'Assistant se Souvenir de Tous les Messages", + "SEND_ALL_INFO": "Si cette option est cochée, tous les messages précédents seront inclus dans l'invite envoyée au chatbot, au lieu des seuls messages de l'échange en cours. " }, "EXCHANGES": { "TITLE": "Échanges Paramètres", + "NAME": "Nom de l'Échange", "DESCRIPTION": "Description des Échanges", "INSTRUCTIONS": "Instructions du Chatbot", "CUE": "Initial Cue", @@ -54,6 +57,7 @@ }, "CONVERSATIONS": { "TITLE": "Voir les Conversations", + "EXPORT_ALL": "Exporter Tout", "TABLE": { "MEMBER": "Membre", "UPDATED": "Dernière modification", @@ -61,7 +65,8 @@ "NOT_STARTED": "Non démarré", "COMPLETE": "Completed", "INCOMPLETE": "Non terminé", - "DELETE": "Supprimer l'interaction", + "DELETE": "Supprimer", + "EXPORT": "Exporter", "NONE": "Aucune conversation jusqu'à présent" }, "RESET": "Supprimer et réinitialiser la conversation" diff --git a/src/modules/context/SettingsContext.tsx b/src/modules/context/SettingsContext.tsx index 4e90674..79a12ef 100644 --- a/src/modules/context/SettingsContext.tsx +++ b/src/modules/context/SettingsContext.tsx @@ -24,14 +24,17 @@ export const defaultSettingsValues: AllSettingsType = { assistantList: [{ id: uuidv4(), name: '', description: '' }], }, chat: { + name: '', description: '', participantInstructions: '', participantEndText: '', + sendAllToChatbot: false, }, exchanges: { exchangeList: [ { id: uuidv4(), + name: '', assistant: { id: '', name: '', diff --git a/src/modules/interaction/ParticipantInteraction.tsx b/src/modules/interaction/ParticipantInteraction.tsx index 4f240c8..b507db9 100644 --- a/src/modules/interaction/ParticipantInteraction.tsx +++ b/src/modules/interaction/ParticipantInteraction.tsx @@ -255,6 +255,7 @@ const ParticipantInteraction = (): ReactElement => { return []; })} participant={currentMember} + sendAllMessages={interaction.sendAllToChatbot} /> ); }; diff --git a/src/modules/message/MessagesPane.tsx b/src/modules/message/MessagesPane.tsx index 66382fe..97ed079 100644 --- a/src/modules/message/MessagesPane.tsx +++ b/src/modules/message/MessagesPane.tsx @@ -39,6 +39,7 @@ type MessagesPaneProps = { participant: Agent; autoDismiss: boolean; goToNextExchange: () => void; + sendAllMessages?: boolean; readOnly?: boolean; }; @@ -51,6 +52,7 @@ const MessagesPane = ({ participant, autoDismiss, goToNextExchange, + sendAllMessages = false, readOnly = false, }: MessagesPaneProps): ReactElement => { // Hook to post chat messages asynchronously using mutation @@ -103,6 +105,7 @@ const MessagesPane = ({ id: uuidv4(), content: currentExchange.participantCue, sender: currentExchange.assistant, + sentAt: new Date(), }, ]); } @@ -145,7 +148,12 @@ const MessagesPane = ({ */ function handlePostChatbot(newMessage: Message): void { // Build the prompt for the chatbot using the existing messages and the new message - const prompt: ChatBotMessage[] = [...buildPrompt(msgs, newMessage)]; + const prompt: ChatBotMessage[] = [ + ...buildPrompt( + [...(sendAllMessages ? pastMessages : []), ...msgs], + newMessage, + ), + ]; // Send the prompt to the chatbot API and handle the response postChatBot(prompt) @@ -154,6 +162,7 @@ const MessagesPane = ({ id: uuidv4(), content: chatBotRes.completion, sender: currentExchange.assistant, + sentAt: new Date(), }; // Add the chatbot's response to the list of messages @@ -174,6 +183,7 @@ const MessagesPane = ({ id: uuidv4(), content, sender: participant, + sentAt: new Date(), }; // Update the messages state with the new message diff --git a/src/results/ConversationsView.tsx b/src/results/ConversationsView.tsx index 2ae67cf..56e2f09 100644 --- a/src/results/ConversationsView.tsx +++ b/src/results/ConversationsView.tsx @@ -2,11 +2,13 @@ import { FC, Fragment } from 'react'; import { UseTranslationResponse, useTranslation } from 'react-i18next'; import DeleteIcon from '@mui/icons-material/Delete'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { Alert, Box, + Button, Collapse, IconButton, Paper, @@ -48,24 +50,14 @@ const Conversations: FC = ({ const { mutate: deleteAppData } = mutations.useDeleteAppData(); // Fetching interaction data - const { data: appDatas } = hooks.useAppData(); + const appDatas = + hooks + .useAppData() + .data?.filter((appData) => appData.type === 'Interaction') || []; // Fetching all members from the app context or defaulting to the checked-out member const allMembers: Member[] = hooks.useAppContext().data?.members || []; - /* - // Memoized value to find the interaction corresponding to the selected member - const checkedOutInteraction: Interaction | undefined = useMemo( - (): Interaction | undefined => - appDatas?.find( - (appData): boolean => - appData?.data?.exchanges?.exchangeList && - appData.member.id === checkedOutMember.id, - )?.data, - [appDatas, checkedOutMember.id], - ); -*/ - const StatusLabel: (started: boolean, complete: boolean) => string = ( started: boolean, complete: boolean, @@ -79,9 +71,88 @@ const Conversations: FC = ({ return t('CONVERSATIONS.TABLE.NOT_STARTED'); }; + // Utility function to convert JSON data to CSV format + const convertJsonToCsv: (data: Interaction[]) => string = ( + data: Interaction[], + ): string => { + const headers: string[] = [ + 'Participant', + 'Sender', + 'Sent at', + 'Exchange', + 'Interaction', + 'Content', + 'Type', + ]; + const csvRows: string[] = [ + headers.join(','), // header row first + ...data.flatMap((interactionData: Interaction): string[] => + interactionData.exchanges.exchangeList.flatMap( + (exchange: Exchange): string[] => + exchange.messages.map((message: Message): string => + [ + interactionData.participant.id, + message.sender.id, + format(new Date(message.sentAt || ''), 'dd/MM/yyyy HH:mm'), + exchange.name, + interactionData.name, + message.content, + typeof message.content, + ].join(','), + ), + ), + ), + ]; + // map data rows + return csvRows.join('\n'); + }; + /* + ...data.map((row) => + headers.map((header) => JSON.stringify(row[header] || '')).join(','), +*/ + // Function to download CSV file + const downloadCsv: (csv: string, filename: string) => void = ( + csv: string, + filename: string, + ): void => { + const blob: Blob = new Blob([csv], { type: 'text/csv' }); + const url: string = window.URL.createObjectURL(blob); + const anchor: HTMLAnchorElement = document.createElement('a'); + anchor.setAttribute('hidden', ''); + anchor.setAttribute('href', url); + anchor.setAttribute('download', filename); + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + }; + + // Main function to handle JSON export as CSV + const exportJsonAsCsv: (jsonData: Interaction[], filename: string) => void = ( + jsonData: Interaction[], + filename: string, + ): void => { + if (jsonData && jsonData.length) { + const csv: string = convertJsonToCsv(jsonData); + downloadCsv(csv, filename); + } + }; + return ( - {t('CONVERSATIONS.TITLE')} + + {t('CONVERSATIONS.TITLE')} + + @@ -91,6 +162,7 @@ const Conversations: FC = ({ {t('CONVERSATIONS.TABLE.UPDATED')} {t('CONVERSATIONS.TABLE.STATUS')} {t('CONVERSATIONS.TABLE.DELETE')} + {t('CONVERSATIONS.TABLE.EXPORT')} @@ -155,6 +227,7 @@ const Conversations: FC = ({ id: interaction?.id || '', }) } + disabled={!interaction} sx={{ width: 'auto' }} > @@ -162,6 +235,19 @@ const Conversations: FC = ({ + + { + exportJsonAsCsv( + interaction ? [interaction.data] : [], + `chatbot_${interaction?.data.description}_${format(new Date(), 'yyyyMMdd_HH.mm')}.csv`, + ); + }} + disabled={!interaction?.data} + > + + + = ({ unmountOnExit > - {interaction?.data ? ( + {interaction?.data?.started ? ( = ({ ); - /* - return ( - - {t('CONVERSATIONS.TITLE')} - - {t('CONVERSATIONS.MEMBER')} - - - {checkedOutMember.id === '' ? null : ( - - {checkedOutInteraction ? ( - - {}} - interactionDescription="" - pastMessages={ - checkedOutInteraction.exchanges.exchangeList.flatMap( - (exchange: Exchange): Message[] => { - // Collect dismissed messages from exchanges - if (exchange.dismissed) { - return exchange.messages; - } - return []; - }, - ) || [] - } - participant={checkedOutInteraction.participant} - autoDismiss={false} - goToNextExchange={(): void => {}} - readOnly - /> - {checkedOutInteraction.completed ? ( - - {t('CONVERSATIONS.TABLE.COMPLETE')} - - ) : ( - - {t('CONVERSATIONS.TABLE.INCOMPLETE')} - - )} - - - - deleteAppData({ - id: - appDatas?.find( - (appData): boolean => - appData.member.id === checkedOutMember.id, - )?.id || '', - }) - } - sx={{ width: 'auto' }} - > - - - - - - - ) : ( - // Show a warning if no interaction is found - - {t('CONVERSATIONS.NONE')} - - )} - - )} - - ); - */ }; export default Conversations; diff --git a/src/settings/ChatSettings.tsx b/src/settings/ChatSettings.tsx index 37446ed..50f7c05 100644 --- a/src/settings/ChatSettings.tsx +++ b/src/settings/ChatSettings.tsx @@ -1,7 +1,8 @@ import { ChangeEvent, FC } from 'react'; import { UseTranslationResponse, useTranslation } from 'react-i18next'; -import { Typography } from '@mui/material'; +import InfoBadge from '@mui/icons-material/Info'; +import { Switch, Tooltip, Typography } from '@mui/material'; import Stack from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; @@ -21,14 +22,24 @@ const ChatSettings: FC = ({ chat, onChange }) => { // Destructuring chat settings const { + name: chatName, description: chatDescription, participantInstructions: chatInstructions, participantEndText: chatEndText, + sendAllToChatbot: chatSendAll, }: ChatSettingsType = chat; return ( {t('SETTINGS.CHAT.TITLE')} + , + ): void => onChange({ ...chat, name: e.target.value })} + /> = ({ chat, onChange }) => { e: ChangeEvent, ): void => onChange({ ...chat, participantEndText: e.target.value })} /> + + {t('SETTINGS.CHAT.SEND_ALL')} + {' '} + + + + + ): void => + onChange({ ...chat, sendAllToChatbot: e.target.checked }) + } + /> ); }; diff --git a/src/settings/ExchangesSettings.tsx b/src/settings/ExchangesSettings.tsx index 6077744..7b3b8fc 100644 --- a/src/settings/ExchangesSettings.tsx +++ b/src/settings/ExchangesSettings.tsx @@ -82,6 +82,7 @@ const ExchangeSettingsPanel: FC = ({ // Destructuring exchange settings const { + name: exchangeName, assistant: exchangeAssistant, description: exchangeDescription, chatbotInstructions: exchangeInstructions, @@ -125,15 +126,15 @@ const ExchangeSettingsPanel: FC = ({ , - ): void => onChange(index, 'description', e.target.value)} + ): void => onChange(index, 'name', e.target.value)} /> + handleMoveUp(index)} @@ -153,6 +154,15 @@ const ExchangeSettingsPanel: FC = ({ + , + ): void => onChange(index, 'description', e.target.value)} + /> = ({ exchanges, onChange }) => { { // Generate a new unique ID id: uuidv4(), + name: '', assistant: { id: '', name: '', diff --git a/src/types/Message.ts b/src/types/Message.ts index 026c7ad..dd12b9f 100644 --- a/src/types/Message.ts +++ b/src/types/Message.ts @@ -6,6 +6,5 @@ export interface Message { id: UUID; content: string; sender: Agent; - updatedAt?: string; - createdAt?: string; + sentAt?: Date; } From 31b5a86a308e1df17fdd5226ad8235cde9a60902 Mon Sep 17 00:00:00 2001 From: deRohrer <131810848+deRohrer@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:41:36 +0200 Subject: [PATCH 2/2] fix: correctly handle creation and patching of app data (#5) * feat: add interaction and exchange names * feat: export id instead of name for debugging purposes * fix: make builder test not fail * fix: allow commas in csv cells * fix: correctly handle creation and patching of appdata * fix: remove appdata showing in builderview --- cypress/e2e/builder/main.cy.ts | 7 +- .../interaction/ParticipantInteraction.tsx | 240 +++++++++++------- src/results/ConversationsView.tsx | 46 ++-- src/types/Agent.ts | 2 +- 4 files changed, 171 insertions(+), 124 deletions(-) diff --git a/cypress/e2e/builder/main.cy.ts b/cypress/e2e/builder/main.cy.ts index 1862b31..6a54016 100644 --- a/cypress/e2e/builder/main.cy.ts +++ b/cypress/e2e/builder/main.cy.ts @@ -2,7 +2,6 @@ import { Context, PermissionLevel } from '@graasp/sdk'; import { BUILDER_VIEW_CY, buildDataCy } from '../../../src/config/selectors'; -/* describe('Builder View', () => { beforeEach(() => { cy.setUpApi( @@ -16,10 +15,6 @@ describe('Builder View', () => { }); it('App', () => { - cy.get(buildDataCy(BUILDER_VIEW_CY)).should( - 'contain.text', - 'Builder as read', - ); + cy.get(buildDataCy(BUILDER_VIEW_CY)).should('contain.text', 'Assistant'); }); }); -*/ diff --git a/src/modules/interaction/ParticipantInteraction.tsx b/src/modules/interaction/ParticipantInteraction.tsx index b507db9..bc72f0e 100644 --- a/src/modules/interaction/ParticipantInteraction.tsx +++ b/src/modules/interaction/ParticipantInteraction.tsx @@ -15,7 +15,14 @@ import { Button } from '@mui/material'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import { LocalContext, useLocalContext } from '@graasp/apps-query-client'; +import { + AppContext, + LocalContext, + useLocalContext, +} from '@graasp/apps-query-client'; +import { Member } from '@graasp/sdk'; + +import { UseQueryResult } from '@tanstack/react-query'; import { defaultAssistant, @@ -38,74 +45,96 @@ const ParticipantInteraction = (): ReactElement => { // Getting the participant ID from local context const { memberId: participantId }: LocalContext = useLocalContext(); - const { data: appDatas } = hooks.useAppData(); + // Fetching application data for interactions + const { data: appDataList, isLoading: appDataLoading } = + hooks.useAppData(); const { mutate: postAppData } = mutations.usePostAppData(); const { mutate: patchAppData } = mutations.usePatchAppData(); - const { chat, exchanges }: SettingsContextType = useSettings(); + // Fetching settings context + const { chat, exchanges }: SettingsContextType = useSettings(); const { t }: UseTranslationResponse<'translations', undefined> = useTranslation(); + // Fetching app member context + const { data: appContextData }: UseQueryResult = + hooks.useAppContext(); + + // Find the member in app context data by participant ID + const appMember: Member | undefined = useMemo( + () => appContextData?.members.find((member) => member.id === participantId), + [appContextData, participantId], + ); + // Define the current member as an agent, merging with the default user - const currentMember: Agent = { - ...defaultUser, - ...hooks - .useAppContext() - // Find the member in app context data by participant ID - .data?.members.find((member) => member.id === participantId), - }; + const currentMember: Agent = useMemo( + (): Agent => ({ + ...defaultUser, + ...(appMember?.id ? { id: appMember.id } : {}), + ...(appMember?.name ? { name: appMember.name } : {}), + }), + [appMember?.id, appMember?.name], + ); /** * @function createInteractionFromTemplate * @description Creates and returns a new `Interaction` object by merging default settings with chat and exchange settings. * @returns {Interaction} A fully constructed `Interaction` object with merged settings. */ - function createInteractionFromTemplate(): Interaction { - // Merge chat settings with default interaction - const interactionBase: Interaction = { - ...defaultInteraction, - ...chat, - participant: currentMember, - }; - interactionBase.exchanges.exchangeList = exchanges.exchangeList.map( - (exchange) => ({ - // Merge default exchange with each exchange from settings - ...defaultExchange, - ...exchange, - assistant: { - ...defaultAssistant, - ...exchange.assistant, - type: AgentType.Assistant, - }, - }), - ); - return interactionBase; - } + const createInteractionFromTemplate: () => Interaction = + useCallback((): Interaction => { + const interactionBase: Interaction = { + ...defaultInteraction, + ...chat, + participant: currentMember, + }; + interactionBase.exchanges.exchangeList = exchanges.exchangeList.map( + (exchange) => ({ + ...defaultExchange, + ...exchange, + assistant: { + ...defaultAssistant, + ...exchange.assistant, + type: AgentType.Assistant, + }, + }), + ); + return interactionBase; + }, [chat, currentMember, exchanges.exchangeList]); // Memoize the current app data for the participant const currentAppData = useMemo( () => - appDatas?.find( - (appData) => - appData?.data?.exchanges && appData.member.id === participantId, - ), - [appDatas, participantId], + appDataList + ?.filter((appData) => appData.type === 'Interaction') + .find((appData) => appData.data.participant.id === participantId), + [appDataList, participantId], ); - // Ref to track if the app data has already been posted - const hasPosted: MutableRefObject = useRef(!!currentAppData); - // State to manage the current interaction, either from existing data or a new template const [interaction, setInteraction]: [ - Interaction, - Dispatch>, - ] = useState( - (currentAppData?.data as Interaction) || createInteractionFromTemplate(), - ); + Interaction | undefined, + Dispatch>, + ] = useState(undefined); + + useEffect((): void => { + if (!appDataLoading && appContextData && appMember) { + setInteraction(currentAppData?.data || createInteractionFromTemplate()); + } + }, [ + appDataLoading, + appContextData, + appMember, + createInteractionFromTemplate, + currentAppData?.data, + ]); + + // Ref to track if the app data has already been posted + const hasPosted: MutableRefObject = useRef(!!currentAppData); // Effect to post the interaction data if it hasn't been posted yet useEffect((): void => { - if (!hasPosted.current) { + if (!hasPosted.current && interaction) { postAppData({ data: interaction, type: 'Interaction' }); hasPosted.current = true; } @@ -113,7 +142,7 @@ const ParticipantInteraction = (): ReactElement => { // Effect to patch the interaction data if it has been posted and current app data exists useEffect((): void => { - if (hasPosted.current && currentAppData?.id) { + if (hasPosted.current && currentAppData?.id && interaction) { patchAppData({ id: currentAppData.id, data: interaction, @@ -124,22 +153,27 @@ const ParticipantInteraction = (): ReactElement => { // Callback to update a specific exchange within the interaction const updateExchange = useCallback((updatedExchange: Exchange): void => { setInteraction( - (prevState: Interaction): Interaction => ({ - ...prevState, - exchanges: { - exchangeList: prevState.exchanges.exchangeList.map((exchange) => - exchange.id === updatedExchange.id ? updatedExchange : exchange, - ), - }, - }), + (prevState: Interaction | undefined): Interaction | undefined => { + if (prevState) { + return { + ...(prevState || defaultInteraction), + exchanges: { + exchangeList: prevState.exchanges.exchangeList.map((exchange) => + exchange.id === updatedExchange.id ? updatedExchange : exchange, + ), + }, + updatedAt: new Date(), + }; + } + return undefined; + }, ); }, []); // Effect to handle actions when the user tries to leave the page (before unload) useEffect(() => { const handleBeforeUnload = (event: BeforeUnloadEvent): string => { - if (!interaction.completed) { - // If the interaction is not completed, prompt the user before leaving + if (!interaction?.completed) { event.preventDefault(); const confirmationMessage = 'Are you sure you want to leave?'; // eslint-disable-next-line no-param-reassign @@ -152,36 +186,44 @@ const ParticipantInteraction = (): ReactElement => { return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; - }, [interaction.completed]); + }, [interaction?.completed]); // Function to start the interaction const startInteraction = (): void => { - setInteraction( - (prev: Interaction): Interaction => ({ - ...prev, - started: true, - startedAt: new Date(), - }), - ); + setInteraction((prev: Interaction | undefined): Interaction | undefined => { + if (prev) { + return { + ...(prev || defaultInteraction), + started: true, + startedAt: new Date(), + updatedAt: new Date(), + }; + } + return undefined; + }); }; // Function to move to the next exchange or complete the interaction const goToNextExchange = (): void => { - setInteraction((prev: Interaction): Interaction => { - const numExchanges: number = prev.exchanges.exchangeList.length; - if (prev.currentExchange === numExchanges - 1) { - // If this is the last exchange, mark the interaction as completed + setInteraction((prev: Interaction | undefined): Interaction | undefined => { + if (prev) { + const numExchanges: number = prev.exchanges.exchangeList.length || 0; + if (prev.currentExchange === numExchanges - 1) { + // If this is the last exchange, mark the interaction as completed + return { + ...prev, + completed: true, + completedAt: new Date(), + updatedAt: new Date(), + }; + } return { ...prev, - completed: true, - completedAt: new Date(), + currentExchange: (prev?.currentExchange || 0) + 1, + updatedAt: new Date(), }; } - return { - ...prev, - // Move to the next exchange - currentExchange: prev.currentExchange + 1, - }; + return undefined; }); }; @@ -191,7 +233,7 @@ const ParticipantInteraction = (): ReactElement => { } // Handle the start of the interaction - const handleStartInteraction: () => void = (): void => { + const handleStartInteraction = (): void => { startInteraction(); }; @@ -222,38 +264,40 @@ const ParticipantInteraction = (): ReactElement => { ); } + // Render the completed interaction message if the interaction is completed - return interaction.completed ? ( - - - {interaction.participantEndText} - - - ) : ( - // Render the MessagesPane component to handle the conversation + if (interaction.completed) { + return ( + + + {interaction.participantEndText} + + + ); + } + + // Render the MessagesPane component to handle the conversation + return ( { - if (exchange.dismissed) { - return exchange.messages; - } - return []; - })} + pastMessages={interaction.exchanges.exchangeList.flatMap((exchange) => + exchange.dismissed ? exchange.messages : [], + )} participant={currentMember} sendAllMessages={interaction.sendAllToChatbot} /> diff --git a/src/results/ConversationsView.tsx b/src/results/ConversationsView.tsx index 56e2f09..9ed3574 100644 --- a/src/results/ConversationsView.tsx +++ b/src/results/ConversationsView.tsx @@ -91,14 +91,19 @@ const Conversations: FC = ({ (exchange: Exchange): string[] => exchange.messages.map((message: Message): string => [ - interactionData.participant.id, - message.sender.id, + interactionData.participant.name, + message.sender.name, format(new Date(message.sentAt || ''), 'dd/MM/yyyy HH:mm'), exchange.name, interactionData.name, message.content, typeof message.content, - ].join(','), + ] + .map( + (cellText: string): string => + `"${cellText.replaceAll('\n', ' ')}"`, + ) + .join(','), ), ), ), @@ -168,10 +173,13 @@ const Conversations: FC = ({ {allMembers && allMembers.map((member, index) => { - const interaction = appDatas?.find( - (appData) => appData.member.id === member.id, + const checkedOutAppData = appDatas?.find( + (appData) => appData.data.participant.id === member.id, ); + const interaction: Interaction | undefined = + checkedOutAppData?.data; + return ( @@ -206,16 +214,16 @@ const Conversations: FC = ({ variant="filled" severity={ // eslint-disable-next-line no-nested-ternary - interaction?.data.completed + interaction?.completed ? 'success' - : interaction?.data.started + : interaction?.started ? 'warning' : 'error' } > {StatusLabel( - interaction?.data.started || false, - interaction?.data.completed || false, + interaction?.started || false, + interaction?.completed || false, )} @@ -224,10 +232,10 @@ const Conversations: FC = ({ color="secondary" onClick={(): void => deleteAppData({ - id: interaction?.id || '', + id: checkedOutAppData?.id || '', }) } - disabled={!interaction} + disabled={!checkedOutAppData} sx={{ width: 'auto' }} > @@ -239,11 +247,11 @@ const Conversations: FC = ({ { exportJsonAsCsv( - interaction ? [interaction.data] : [], - `chatbot_${interaction?.data.description}_${format(new Date(), 'yyyyMMdd_HH.mm')}.csv`, + interaction ? [interaction] : [], + `chatbot_${interaction?.description}_${format(new Date(), 'yyyyMMdd_HH.mm')}.csv`, ); }} - disabled={!interaction?.data} + disabled={!interaction} > @@ -260,18 +268,18 @@ const Conversations: FC = ({ unmountOnExit > - {interaction?.data?.started ? ( + {interaction?.started ? ( {}} interactionDescription="" pastMessages={ - interaction.data.exchanges.exchangeList.flatMap( + interaction.exchanges.exchangeList.flatMap( (exchange: Exchange): Message[] => { // Collect dismissed messages from exchanges if (exchange.dismissed) { @@ -281,7 +289,7 @@ const Conversations: FC = ({ }, ) || [] } - participant={interaction.data.participant} + participant={interaction.participant} autoDismiss={false} goToNextExchange={(): void => {}} readOnly diff --git a/src/types/Agent.ts b/src/types/Agent.ts index 8ac0f7a..2033bb6 100644 --- a/src/types/Agent.ts +++ b/src/types/Agent.ts @@ -4,9 +4,9 @@ import AgentType from '@/types/AgentType'; type Agent = { id: UUID; + name: string; type: AgentType; description?: string; - name: string; imageUrl?: string; };