diff --git a/.gitignore b/.gitignore index b3f4503..eb5868e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ yarn-debug.log* cypress/screenshots/ cypress/videos/ cypress/downloads/ + +#vite +vite.config.ts.timestamp-* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 08af675..dc08065 100644 --- a/.prettierrc +++ b/.prettierrc @@ -13,5 +13,6 @@ ], "importOrderSeparation": true, "importOrderSortSpecifiers": true, - "plugins": ["@trivago/prettier-plugin-sort-imports"] + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "endOfLine": "auto" } diff --git a/.yarnrc.yml b/.yarnrc.yml index 6bc5e6d..75e896b 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,6 +1,6 @@ compressionLevel: mixed -defaultSemverRangePrefix: "" +defaultSemverRangePrefix: '' enableGlobalCache: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 22592bd..e7d357a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,39 +2,37 @@ ## 1.0.0 (2024-04-14) - ### Features -* add one thread and auto dismiss options ([273b267](https://github.com/graasp/graasp-app-botticelli/commit/273b267e17931f6f5f5fa28a6b01aba4774dc1ed)) -* add scrolling ([3d09ac7](https://github.com/graasp/graasp-app-botticelli/commit/3d09ac76d51e94c930fcd73aeda6ba7466052824)) -* blur after sending message ([13f83e6](https://github.com/graasp/graasp-app-botticelli/commit/13f83e6283031b08f5be15d9202429f92e6df7d9)) -* improve prompts ([bbfca97](https://github.com/graasp/graasp-app-botticelli/commit/bbfca970479a9b655c2d2863dc640e4856020ba6)) -* make height full viewport ([ef95461](https://github.com/graasp/graasp-app-botticelli/commit/ef95461d54c569ec6c7390537965e794e1d0ad65)) -* save app data ([6db1c6a](https://github.com/graasp/graasp-app-botticelli/commit/6db1c6a0228544eedfdbf50e9dd512932fd4aac7)) -* save chat in session storage ([c8c6ed0](https://github.com/graasp/graasp-app-botticelli/commit/c8c6ed046fe36cfb6eedae3ed2e9e775b94bedfe)) -* show warning on unload ([3435ddf](https://github.com/graasp/graasp-app-botticelli/commit/3435ddfd840b697487c5db8c644796e8e09e0dca)) -* update prompt ([3736f05](https://github.com/graasp/graasp-app-botticelli/commit/3736f05ce53fa10b491f981fe9d1213472f3241e)) - +- add one thread and auto dismiss options ([273b267](https://github.com/graasp/graasp-app-botticelli/commit/273b267e17931f6f5f5fa28a6b01aba4774dc1ed)) +- add scrolling ([3d09ac7](https://github.com/graasp/graasp-app-botticelli/commit/3d09ac76d51e94c930fcd73aeda6ba7466052824)) +- blur after sending message ([13f83e6](https://github.com/graasp/graasp-app-botticelli/commit/13f83e6283031b08f5be15d9202429f92e6df7d9)) +- improve prompts ([bbfca97](https://github.com/graasp/graasp-app-botticelli/commit/bbfca970479a9b655c2d2863dc640e4856020ba6)) +- make height full viewport ([ef95461](https://github.com/graasp/graasp-app-botticelli/commit/ef95461d54c569ec6c7390537965e794e1d0ad65)) +- save app data ([6db1c6a](https://github.com/graasp/graasp-app-botticelli/commit/6db1c6a0228544eedfdbf50e9dd512932fd4aac7)) +- save chat in session storage ([c8c6ed0](https://github.com/graasp/graasp-app-botticelli/commit/c8c6ed046fe36cfb6eedae3ed2e9e775b94bedfe)) +- show warning on unload ([3435ddf](https://github.com/graasp/graasp-app-botticelli/commit/3435ddfd840b697487c5db8c644796e8e09e0dca)) +- update prompt ([3736f05](https://github.com/graasp/graasp-app-botticelli/commit/3736f05ce53fa10b491f981fe9d1213472f3241e)) ### Bug Fixes -* attempt to make warning work on mobile ([7333e99](https://github.com/graasp/graasp-app-botticelli/commit/7333e99e25d4bcfe2aa23d03087412ac61ecbd2f)) -* **deps:** update dependency @emotion/styled to v11.11.5 ([ed3c6f1](https://github.com/graasp/graasp-app-botticelli/commit/ed3c6f1cb205b94e8063c18736541b6dbf4c162d)) -* **deps:** update dependency @graasp/apps-query-client to v3.4.10 ([ff23dbb](https://github.com/graasp/graasp-app-botticelli/commit/ff23dbb6b35c5d42ea5dc2de2aa8f89e3e4b8a4e)) -* **deps:** update dependency @graasp/sdk to v4.6.0 ([03c8d5f](https://github.com/graasp/graasp-app-botticelli/commit/03c8d5f45c19b5995be54ddc9d99b872c828cb3b)) -* **deps:** update dependency @graasp/sdk to v4.7.1 ([afd90bb](https://github.com/graasp/graasp-app-botticelli/commit/afd90bb6ee850a7290c45bb6b1fa2ce8dced000e)) -* **deps:** update dependency @types/react to v18.2.75 ([54c503e](https://github.com/graasp/graasp-app-botticelli/commit/54c503e35dc864f3b3f0d4e42fbee2e41f67d057)) -* **deps:** update dependency typescript to v5.4.4 ([ce757e6](https://github.com/graasp/graasp-app-botticelli/commit/ce757e6cc1b2303a6be614622a1e9cd77238e749)) -* **deps:** update dependency typescript to v5.4.5 ([7c24658](https://github.com/graasp/graasp-app-botticelli/commit/7c24658a953a2ecace3394dcd038871410fbf73c)) -* **deps:** update mui (non-major) ([d206f8d](https://github.com/graasp/graasp-app-botticelli/commit/d206f8daf8be8562a76f47208fe287e028191e1f)) -* **deps:** update react monorepo ([ed7e82a](https://github.com/graasp/graasp-app-botticelli/commit/ed7e82ad865b207b0a71a37d960b996bf7793af3)) -* **deps:** update react monorepo ([7fe3b03](https://github.com/graasp/graasp-app-botticelli/commit/7fe3b03771a347dc670816a6615c60d2c289b9a8)) -* fix player view test ([365ee8a](https://github.com/graasp/graasp-app-botticelli/commit/365ee8a0c79a1dd80b4742c148adae248809d250)) -* fix prompt builder helper ([c0f453a](https://github.com/graasp/graasp-app-botticelli/commit/c0f453a5be8a32773e16aaabf6c7a5079af79800)) -* fix size of instructions ([f12013d](https://github.com/graasp/graasp-app-botticelli/commit/f12013d6dee686e17b45cfc827309fb9525287af)) -* fix tests ([8955748](https://github.com/graasp/graasp-app-botticelli/commit/895574856d7b3dc53f096804fc89f273e9dd5cf2)) -* fix window access ([78b7519](https://github.com/graasp/graasp-app-botticelli/commit/78b7519f75149093773c82156ac675138d061226)) -* remove autoscrolling ([b6a1a06](https://github.com/graasp/graasp-app-botticelli/commit/b6a1a0615eba2b136515e41291af13826ca7df6e)) -* set status to idle after response ([6084db3](https://github.com/graasp/graasp-app-botticelli/commit/6084db3e483e2db3e7b18f3a232eb2a9b8bf903b)) -* update cues ([568ff8a](https://github.com/graasp/graasp-app-botticelli/commit/568ff8aec44dcd7e506850b5badc068ff8599490)) -* update text ([a34c545](https://github.com/graasp/graasp-app-botticelli/commit/a34c5459e78fb53b5c871cd134f29e49dd7062f0)) +- attempt to make warning work on mobile ([7333e99](https://github.com/graasp/graasp-app-botticelli/commit/7333e99e25d4bcfe2aa23d03087412ac61ecbd2f)) +- **deps:** update dependency @emotion/styled to v11.11.5 ([ed3c6f1](https://github.com/graasp/graasp-app-botticelli/commit/ed3c6f1cb205b94e8063c18736541b6dbf4c162d)) +- **deps:** update dependency @graasp/apps-query-client to v3.4.10 ([ff23dbb](https://github.com/graasp/graasp-app-botticelli/commit/ff23dbb6b35c5d42ea5dc2de2aa8f89e3e4b8a4e)) +- **deps:** update dependency @graasp/sdk to v4.6.0 ([03c8d5f](https://github.com/graasp/graasp-app-botticelli/commit/03c8d5f45c19b5995be54ddc9d99b872c828cb3b)) +- **deps:** update dependency @graasp/sdk to v4.7.1 ([afd90bb](https://github.com/graasp/graasp-app-botticelli/commit/afd90bb6ee850a7290c45bb6b1fa2ce8dced000e)) +- **deps:** update dependency @types/react to v18.2.75 ([54c503e](https://github.com/graasp/graasp-app-botticelli/commit/54c503e35dc864f3b3f0d4e42fbee2e41f67d057)) +- **deps:** update dependency typescript to v5.4.4 ([ce757e6](https://github.com/graasp/graasp-app-botticelli/commit/ce757e6cc1b2303a6be614622a1e9cd77238e749)) +- **deps:** update dependency typescript to v5.4.5 ([7c24658](https://github.com/graasp/graasp-app-botticelli/commit/7c24658a953a2ecace3394dcd038871410fbf73c)) +- **deps:** update mui (non-major) ([d206f8d](https://github.com/graasp/graasp-app-botticelli/commit/d206f8daf8be8562a76f47208fe287e028191e1f)) +- **deps:** update react monorepo ([ed7e82a](https://github.com/graasp/graasp-app-botticelli/commit/ed7e82ad865b207b0a71a37d960b996bf7793af3)) +- **deps:** update react monorepo ([7fe3b03](https://github.com/graasp/graasp-app-botticelli/commit/7fe3b03771a347dc670816a6615c60d2c289b9a8)) +- fix player view test ([365ee8a](https://github.com/graasp/graasp-app-botticelli/commit/365ee8a0c79a1dd80b4742c148adae248809d250)) +- fix prompt builder helper ([c0f453a](https://github.com/graasp/graasp-app-botticelli/commit/c0f453a5be8a32773e16aaabf6c7a5079af79800)) +- fix size of instructions ([f12013d](https://github.com/graasp/graasp-app-botticelli/commit/f12013d6dee686e17b45cfc827309fb9525287af)) +- fix tests ([8955748](https://github.com/graasp/graasp-app-botticelli/commit/895574856d7b3dc53f096804fc89f273e9dd5cf2)) +- fix window access ([78b7519](https://github.com/graasp/graasp-app-botticelli/commit/78b7519f75149093773c82156ac675138d061226)) +- remove autoscrolling ([b6a1a06](https://github.com/graasp/graasp-app-botticelli/commit/b6a1a0615eba2b136515e41291af13826ca7df6e)) +- set status to idle after response ([6084db3](https://github.com/graasp/graasp-app-botticelli/commit/6084db3e483e2db3e7b18f3a232eb2a9b8bf903b)) +- update cues ([568ff8a](https://github.com/graasp/graasp-app-botticelli/commit/568ff8aec44dcd7e506850b5badc068ff8599490)) +- update text ([a34c545](https://github.com/graasp/graasp-app-botticelli/commit/a34c5459e78fb53b5c871cd134f29e49dd7062f0)) diff --git a/README.md b/README.md index 29de754..39833bf 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ A version of Botticelli's participant interface that runs on the Graasp platform ### Credits -Uses Graasp's app template as a starter. \ No newline at end of file +Uses Graasp's app template as a starter. diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html index e39ba42..faf3b5f 100644 --- a/cypress/support/component-index.html +++ b/cypress/support/component-index.html @@ -1,4 +1,4 @@ - + diff --git a/renovate.json b/renovate.json index 61ad54a..c4f32b3 100644 --- a/renovate.json +++ b/renovate.json @@ -1,16 +1,23 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "github>graasp/renovate-config:app" - ], + "extends": ["github>graasp/renovate-config:app"], "packageRules": [ { "matchDepTypes": ["devDependencies"], - "matchPackagePatterns": ["lint", "prettier", "vite", "cypress", "commitlint", "axios", "concurrently", "env"], + "matchPackagePatterns": [ + "lint", + "prettier", + "vite", + "cypress", + "commitlint", + "axios", + "concurrently", + "env" + ], "automerge": true }, { - "matchUpdateTypes": ["minor","patch"], + "matchUpdateTypes": ["minor", "patch"], "matchCurrentVersion": "!/^0/", "automerge": true } diff --git a/src/config/appData.ts b/src/config/appData.ts deleted file mode 100644 index ca81b01..0000000 --- a/src/config/appData.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AppDataTypes { - ParticipantComment = 'participant-comment', - AssistantComment = 'assistant-comment', -} diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts new file mode 100644 index 0000000..d2a3096 --- /dev/null +++ b/src/config/appSettings.ts @@ -0,0 +1,28 @@ +import { UUID } from '@graasp/sdk'; + +import Agent from '@/types/Agent'; + +type AssistantSettings = Omit; + +export type AssistantsSettingsType = { + assistantList: AssistantSettings[]; +}; + +export type ChatSettingsType = { + description: string; + participantInstructions: string; + participantEndText: string; +}; + +export type ExchangeSettings = { + id: UUID; + assistant: AssistantSettings; + description: string; + chatbotInstructions: string; + participantCue: string; + nbFollowUpQuestions: number; + participantInstructionsOnComplete?: string; + hardLimit: boolean; +}; + +export type ExchangesSettingsType = { exchangeList: ExchangeSettings[] }; diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..5b2459a --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,61 @@ +import { Member } from '@graasp/sdk'; + +import { t } from 'i18next'; +import { v4 as uuidv4 } from 'uuid'; + +import { defaultSettingsValues } from '@/modules/context/SettingsContext'; +import Agent from '@/types/Agent'; +import AgentType from '@/types/AgentType'; +import Exchange from '@/types/Exchange'; +import Interaction from '@/types/Interaction'; + +export const MIN_FOLLOW_UP_QUESTIONS: number = 0; +export const MAX_FOLLOW_UP_QUESTIONS: number = 400; +export const MAX_TEXT_INPUT_CHARS: number = 5000; + +// Define a default user as an agent +export const defaultUser: Agent = { + id: uuidv4(), + name: 'Default User', + description: 'Default user description', + type: AgentType.User, +}; + +// Define a default assistant as an agent +export const defaultAssistant: Agent = { + id: uuidv4(), + name: 'Default Assistant', + description: 'Default assistant description', + type: AgentType.Assistant, +}; + +// Define a default interaction object using default settings +export const defaultInteraction: Interaction = { + ...defaultSettingsValues.chat, + id: uuidv4(), + currentExchange: 0, + started: false, + completed: false, + participant: defaultUser, + exchanges: { exchangeList: [] }, + createdAt: new Date(), + updatedAt: new Date(), +}; + +// Define a default exchange object using default settings +export const defaultExchange: Exchange = { + ...defaultSettingsValues.exchanges.exchangeList[0], + messages: [], + assistant: defaultAssistant, + started: false, + completed: false, + dismissed: false, + createdAt: new Date(), + updatedAt: new Date(), +}; + +export const placeholderMember: Member = { + id: '', + name: t('CONVERSATIONS.PLACEHOLDER'), + email: '', +}; diff --git a/src/config/i18n.ts b/src/config/i18n.ts index 015e49a..6021bb2 100644 --- a/src/config/i18n.ts +++ b/src/config/i18n.ts @@ -27,7 +27,7 @@ i18n.use(initReactI18next).init({ debug: import.meta.env.DEV, ns: [defaultNS], defaultNS, - keySeparator: false, + keySeparator: '.', interpolation: { escapeValue: false, formatSeparator: ',', diff --git a/src/config/messages.ts b/src/config/messages.ts deleted file mode 100644 index b752742..0000000 --- a/src/config/messages.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error has occurred.'; -export const SUCCESS_MESSAGE = 'The operation succeeded.'; diff --git a/src/langs/en.json b/src/langs/en.json index f8f4256..2b5a2b3 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -1,6 +1,5 @@ { "translations": { - "Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit", "ERROR_BOUNDARY": { "FALLBACK": { "MESSAGE_TITLE": "Sorry, something went wrong with this application", @@ -15,6 +14,58 @@ "THANKS_FOR_FEEDBACK": "Thank you for your feedback!", "SEND": "Send your feedback" } + }, + "SETTINGS": { + "TITLE": "App Settings", + "SAVE_BTN": "Save", + "UP": "Move up", + "DOWN": "Move down", + "ASSISTANTS": { + "TITLE": "Assistants Settings", + "ID": "Assistant ID", + "NAME": "Assistant Name", + "DESCRIPTION": "Assistant Description", + "IMAGE": "Assistant Image", + "URL": "Please enter a working link to your desired image.", + "ADD": "Create New Assistant", + "CREATE": "Please create at least one assistant." + }, + "CHAT": { + "TITLE": "Chat Settings", + "DESCRIPTION": "Chat Description", + "INSTRUCTIONS": "Participant Instructions", + "END": "End Screen Text", + "USER": "Participant" + }, + "EXCHANGES": { + "TITLE": "Exchanges Settings", + "DESCRIPTION": "Exchanges Description", + "INSTRUCTIONS": "Chatbot instructions", + "CUE": "Initial Cue", + "ASSISTANT": "Assistant", + "CREATE_ASSISTANT": "Please create at least one assistant.", + "FOLLOW_UP_QUESTIONS": "Number of Follow Up Questions", + "ON_COMPLETE": "Instruction on Exchange Completion", + "ON_COMPLETE_HELPER": "This text will be shown once the given number of follow up questions have been answered. Leave blank for no message.", + "DISABLE_HARD_LIMIT": "Auto Dismiss", + "HARD_LIMIT_INFO": "If checked, the next exchange will start automatically after the number of follow up questions is reached.", + "CREATE": "Please create at least one exchange." + } + }, + "CONVERSATIONS": { + "TITLE": "View Conversations", + "MEMBER": "Please select a member", + "PLACEHOLDER": "Please select a member", + "NONE": "No conversations so far.", + "COMPLETE": "Completed", + "INCOMPLETE": "Not completed", + "RESET": "Delete and reset conversation" + }, + "START": "Start", + "MESSAGE_BOX": { + "INSERT_HERE": "Your response here...", + "SEND": "Send", + "DONE": "Done" } } } diff --git a/src/langs/fr.json b/src/langs/fr.json index f9e97c8..5b0eae9 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -1,5 +1,71 @@ { "translations": { - "Welcome to the Graasp App Starter Kit": "Bienvenue dans le kit de démarrage de l'application Graasp" + "ERROR_BOUNDARY": { + "FALLBACK": { + "MESSAGE_TITLE": "Désolé, quelque chose n'a pas fonctionné avec cette application", + "MESSAGE_FEEDBACK": "Notre équipe a été informée. Si vous souhaitez nous aider, veuillez nous expliquer ce qui s'est passé ci-dessous", + "ERROR_DETAILS": "Détails de l'erreur", + "NAME_LABEL": "Name", + "NAME_HELPER": "Fournissez votre nom (facultatif)", + "EMAIL_LABEL": "Email", + "EMAIL_HELPER": "Fournissez votre courriel (facultatif)", + "COMMENT_LABEL": "Commentaire", + "COMMENT_HELPER": "Racontez-nous ce qui s'est passé (facultatif)", + "THANKS_FOR_FEEDBACK": "Thank you for your feedback !", + "SEND": "Envoyez votre retour d'information" + } + }, + "SETTINGS": { + "TITLE": "Paramètres de l'application", + "SAVE_BTN": "Enregistrer", + "UP": "Déplacer vers le haut", + "DOWN": "Déplacer vers le bas", + "ASSISTANTS": { + "TITLE": "Paramètres de l'Assistant", + "ID": "Assistant ID", + "NAME": "Nom de l'Assistant", + "DESCRIPTION": "Description de l'Assistant", + "IMAGE": "Image de l'Assistant", + "URL": "Veuillez saisir un lien fonctionnel vers l'image souhaitée.", + "ADD": "Créer un Nouvel Assistant", + "CREATE": "Veuillez créer au moins un assistant." + }, + "CHAT": { + "TITLE": " Paramètres du Chat", + "DESCRIPTION": "Description du Chat", + "INSTRUCTIONS": "Instructions pour le Participant", + "END": "Texte de Fin d'Écran", + "USER": "Participant" + }, + "EXCHANGES": { + "TITLE": "Échanges Paramètres", + "DESCRIPTION": "Description des Échanges", + "INSTRUCTIONS": "Instructions du Chatbot", + "CUE": "Initial Cue", + "ASSISTANT": "Assistant", + "CREATE_ASSISTANT": "Veuillez créer au moins un assistant.", + "FOLLOW_UP_QUESTIONS": "Nombre de Questions de Suivi", + "ON_COMPLETE": "Instruction sur l'achèvement de l'échange", + "ON_COMPLETE_HELPER": "Ce texte s'affichera une fois que le nombre donné de questions de suivi aura été répondu. Laisser en blanc si aucun message n'est affiché.", + "DISABLE_HARD_LIMIT": "Rejet Automatique", + "HARD_LIMIT_INFO": "Si cette case est cochée, l'échange suivant commencera automatiquement une fois que le nombre de questions de suivi sera atteint", + "CREATE": "Veuillez créer au moins un échange." + } + }, + "CONVERSATIONS": { + "TITLE": "Voir les Conversations", + "MEMBER": "Veuillez sélectionner un membre", + "PLACEHOLDER": "Veuillez sélectionner un membre", + "NONE": "Aucune conversation jusqu'à présent.", + "COMPLETE": "Terminé", + "INCOMPLETE": "Pas terminé", + "RESET": "Supprimer et réinitialiser la conversation" + }, + "START": "Démarrer", + "MESSAGE_BOX": { + "INSERT_HERE": "Votre réponse ici...", + "SEND": "Envoyer", + "DONE": "Fini" + } } } diff --git a/src/modules/context/SettingsContext.tsx b/src/modules/context/SettingsContext.tsx index 31aa2cb..4e90674 100644 --- a/src/modules/context/SettingsContext.tsx +++ b/src/modules/context/SettingsContext.tsx @@ -1,23 +1,64 @@ import { FC, ReactElement, createContext, useContext } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { + AssistantsSettingsType, + ChatSettingsType, + ExchangesSettingsType, +} from '@/config/appSettings'; + import { hooks, mutations } from '../../config/queryClient'; import Loader from '../common/Loader'; // mapping between Setting names and their data type -// eslint-disable-next-line @typescript-eslint/ban-types -type AllSettingsType = {}; +type AllSettingsType = { + assistants: AssistantsSettingsType; + chat: ChatSettingsType; + exchanges: ExchangesSettingsType; +}; // default values for the data property of settings by name -const defaultSettingsValues: AllSettingsType = {}; +export const defaultSettingsValues: AllSettingsType = { + assistants: { + assistantList: [{ id: uuidv4(), name: '', description: '' }], + }, + chat: { + description: '', + participantInstructions: '', + participantEndText: '', + }, + exchanges: { + exchangeList: [ + { + id: uuidv4(), + assistant: { + id: '', + name: '', + description: '', + }, + description: '', + chatbotInstructions: '', + participantCue: '', + participantInstructionsOnComplete: '', + nbFollowUpQuestions: 0, + hardLimit: false, + }, + ], + }, +}; // list of the settings names const ALL_SETTING_NAMES = [ // name of your settings + 'assistants', + 'chat', + 'exchanges', ] as const; // automatically generated types type AllSettingsNameType = (typeof ALL_SETTING_NAMES)[number]; -type AllSettingsDataType = AllSettingsType[keyof AllSettingsType]; +export type AllSettingsDataType = AllSettingsType[keyof AllSettingsType]; export type SettingsContextType = AllSettingsType & { saveSettings: ( @@ -75,14 +116,17 @@ export const SettingsProvider: FC = ({ children }) => { if (isSuccess) { const allSettings: AllSettingsType = ALL_SETTING_NAMES.reduce( (acc: AllSettingsType, key: T) => { - // todo: types are not inferred correctly here - // @ts-ignore const setting = appSettingsList.find((s) => s.name === key); - const settingData = setting?.data; - acc[key] = settingData as AllSettingsType[T]; + if (setting) { + const settingData = + setting?.data as unknown as AllSettingsType[typeof key]; + acc[key] = settingData; + } else { + acc[key] = defaultSettingsValues[key]; + } return acc; }, - {}, + defaultSettingsValues, ); return { ...allSettings, diff --git a/src/modules/interaction/ParticipantInteraction.tsx b/src/modules/interaction/ParticipantInteraction.tsx index 9604818..e4d6bdc 100644 --- a/src/modules/interaction/ParticipantInteraction.tsx +++ b/src/modules/interaction/ParticipantInteraction.tsx @@ -1,147 +1,138 @@ -import { ReactElement, useEffect, useState } from 'react'; +import { + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; import { Button } from '@mui/material'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; +import { useLocalContext } from '@graasp/apps-query-client'; + +import { + defaultAssistant, + defaultExchange, + defaultInteraction, + defaultUser, +} from '@/config/config'; +import { hooks, mutations } from '@/config/queryClient'; import { START_INTERACTION_BUTTON_CY } from '@/config/selectors'; import MessagesPane from '@/modules/message/MessagesPane'; import Agent from '@/types/Agent'; import AgentType from '@/types/AgentType'; +import Exchange from '@/types/Exchange'; import Interaction from '@/types/Interaction'; -const ParticipantInteraction = (): ReactElement => { - const participantId = '0'; - - const artificialAssistant: Agent = { - id: '1', - name: 'Interviewer', - description: 'Assistant Description', - type: AgentType.Assistant, - }; +import { useSettings } from '../context/SettingsContext'; - const defaultInteraction: Interaction = { - id: 0, - description: 'Default Description', - modelInstructions: '', - participantInstructions: `Bienvenue et merci de participer à notre étude sur les images mentales induites par l'écoute profonde (deep listening), images qui font font partie de l'œuvre. Un agent conversationnel vous posera quelques questions pour vous aider à décrire ce que vous avez perçu pendant l'écoute du concert de Luca Forcucci. Vos réponses sont entièrement anonymes et aucune donnée personnelle ne vous sera demandée (ne fournissez pas d'information personnelle afin de ne pas ouvrir la possibilité d'être identifié-e).`, - participantInstructionsOnComplete: `Merci beaucoup! Vos indications nous seront précieuses pour évaluer l'occurrence et la nature des sensations induites par l'écoute. Cette évaluation fait partie de l'œuvre, et aide à la composition de nouvelles formes musicales.`, - name: 'Default Name', - currentExchange: 0, - started: false, - completed: false, - participant: { - id: participantId, - type: AgentType.Assistant, - description: 'User Description', - name: 'User', - }, - exchanges: [ - { - id: 0, - name: 'Exchange 1', - description: 'Exchange 1 Description', - instructions: 'Instructions', - participantInstructionsOnComplete: ``, - cue: `Quelles sont les images mentales les plus fortes ou les plus claires que vous avez perçues pendant l'écoute du concert? Ce pourraient être des visions brèves pendant un moment de somnolence, ou des images persistantes qui vous sont apparues (les yeux ouverts ou fermés).`, - order: 0, - messages: [], - assistant: artificialAssistant, - triggers: [], - started: false, - completed: false, - dismissed: false, - softLimit: 5, - hardLimit: 0, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: 1, - name: 'Exchange 2', - description: 'Exchange 2 Description', - instructions: 'Instructions', - participantInstructionsOnComplete: ``, - cue: `Merci! À présent, pourriez-vous décrire s'il s'agissait plus de formes réelles ou imaginaires? Réalistes ou abstraites?`, - order: 2, - messages: [], - assistant: artificialAssistant, - triggers: [], - started: false, - completed: false, - dismissed: false, - softLimit: 5, - hardLimit: 0, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: 2, - name: 'Exchange 3', - description: 'Exchange 3 Description', - instructions: 'Instructions', - participantInstructionsOnComplete: ``, - cue: `Merci! Une dernière question s’il vous plaît! Où se trouvait votre corps par rapport à ces images? Vous les observiez depuis un point de vue extérieur, depuis le bas ou le haut ou latéralement, ou alors aviez-vous la sensation d'être immergé dans un espace qui vous entoure, d'être transporté dans un lieu?`, - order: 3, - messages: [], - assistant: artificialAssistant, - triggers: [], - started: false, - completed: false, - dismissed: false, - softLimit: 5, - hardLimit: 0, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: 3, - name: 'Exchange 4', - description: 'Exchange 4 Description', - instructions: 'Instructions', - participantInstructionsOnComplete: ``, - cue: `Merci beaucoup pour vos réponses! Avez-vous quelque chose à ajouter?`, - order: 4, - messages: [], - assistant: artificialAssistant, - triggers: [], - started: false, - completed: false, - dismissed: false, - softLimit: 1, - hardLimit: 0, - createdAt: new Date(), - updatedAt: new Date(), - }, - ], - createdAt: new Date(), - updatedAt: new Date(), +// Main component: ParticipantInteraction +const ParticipantInteraction = (): ReactElement => { + // Getting the participant ID from local context + const { memberId: participantId } = useLocalContext(); + + const { data: appDatas } = hooks.useAppData(); + const { mutate: postAppData } = mutations.usePostAppData(); + const { mutate: patchAppData } = mutations.usePatchAppData(); + const { chat, exchanges } = useSettings(); + + const { t } = useTranslation(); + + // 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), }; - function load(key: string): Interaction { - const item = window.sessionStorage.getItem(key); - return item != null ? JSON.parse(item) : defaultInteraction; + /** + * @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; } + // Memoize the current app data for the participant + const currentAppData = useMemo( + () => + appDatas?.find( + (appData) => + appData?.data?.exchanges && appData.member.id === participantId, + ), + [appDatas, participantId], + ); + + // Ref to track if the app data has already been posted + const hasPosted = useRef(!!currentAppData); + + // State to manage the current interaction, either from existing data or a new template const [interaction, setInteraction] = useState( - load('interaction'), + (currentAppData?.data as Interaction) || createInteractionFromTemplate(), ); + // Effect to post the interaction data if it hasn't been posted yet + useEffect(() => { + if (!hasPosted.current) { + postAppData({ data: interaction, type: 'Interaction' }); + hasPosted.current = true; + } + }, [interaction, postAppData]); + + // Effect to patch the interaction data if it has been posted and current app data exists useEffect(() => { - window.sessionStorage.setItem('interaction', JSON.stringify(interaction)); - }, [interaction]); + if (hasPosted.current && currentAppData?.id) { + patchAppData({ + id: currentAppData.id, + data: interaction, + }); + } + }, [interaction, patchAppData, currentAppData?.id]); + + // Callback to update a specific exchange within the interaction + const updateExchange = useCallback((updatedExchange: Exchange): void => { + setInteraction((prevState) => ({ + ...prevState, + exchanges: { + exchangeList: prevState.exchanges.exchangeList.map((exchange) => + exchange.id === updatedExchange.id ? updatedExchange : exchange, + ), + }, + })); + }, []); + // Effect to handle actions when the user tries to leave the page (before unload) useEffect(() => { const handleBeforeUnload = (event: BeforeUnloadEvent): string => { if (!interaction.completed) { - // Perform actions before the component unloads + // If the interaction is not completed, prompt the user before leaving event.preventDefault(); - - // Chrome requires returnValue to be set - // Prompt the user before leaving const confirmationMessage = 'Are you sure you want to leave?'; - - // Chrome requires returnValue to be set // eslint-disable-next-line no-param-reassign event.returnValue = confirmationMessage; // For Chrome return confirmationMessage; // For standard browsers @@ -152,37 +143,48 @@ const ParticipantInteraction = (): ReactElement => { return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; - }, [interaction]); - - function startInteraction(): void { - const updatedInteraction = { ...interaction }; - updatedInteraction.started = true; - updatedInteraction.startedAt = new Date(); - setInteraction(updatedInteraction); - } + }, [interaction.completed]); + + // Function to start the interaction + const startInteraction = (): void => { + setInteraction((prev) => ({ + ...prev, + started: true, + startedAt: new Date(), + })); + }; + // Function to move to the next exchange or complete the interaction const goToNextExchange = (): void => { - const updatedInteraction = { ...interaction }; - const numExchanges = interaction.exchanges.length; - const { currentExchange } = interaction; - if (currentExchange === numExchanges - 1) { - updatedInteraction.completed = true; - updatedInteraction.completedAt = new Date(); - setInteraction(updatedInteraction); - } else { - updatedInteraction.currentExchange = interaction.currentExchange + 1; - setInteraction(updatedInteraction); - } + setInteraction((prev) => { + const numExchanges = prev.exchanges.exchangeList.length; + if (prev.currentExchange === numExchanges - 1) { + // If this is the last exchange, mark the interaction as completed + return { + ...prev, + completed: true, + completedAt: new Date(), + }; + } + return { + ...prev, + // Move to the next exchange + currentExchange: prev.currentExchange + 1, + }; + }); }; + // Render fallback if interaction data is not available if (!interaction) { return
Interaction Not Found
; } + // Handle the start of the interaction const handleStartInteraction = (): void => { startInteraction(); }; + // Render the start interaction button if the interaction has not started if (!interaction.started) { return ( { }} > {interaction.participantInstructions && ( - <> - - {interaction.participantInstructions} - - - Le dialogue dure environ 5 minutes et vous recevrez une petite - récompense pour vous remercier de votre participation! - - + + {interaction.participantInstructions} + )} ); } - + // Render the completed interaction message if the interaction is completed return interaction.completed ? ( { }} > - {interaction.participantInstructionsOnComplete} -
-
- Cadeau! Écoutez ces concerts de Luca Forcucci en streaming! -
-
- - https://vimeo.com/manage/videos/659021910 - -
- - https://tinyurl.com/2w7bj2xs - -
- - https://tinyurl.com/526xxa8w - -
-
- - Plus d’information sur l’artiste. - -
- - Plus d’information sur les chercheurs. - + {interaction.participantEndText}
) : ( + // Render the MessagesPane component to handle the conversation { + if (exchange.dismissed) { + return exchange.messages; + } + return []; + })} + participant={currentMember} /> ); }; +// Export the ParticipantInteraction component as the default export export default ParticipantInteraction; diff --git a/src/modules/main/BuilderView.tsx b/src/modules/main/BuilderView.tsx index a654a7e..6796319 100644 --- a/src/modules/main/BuilderView.tsx +++ b/src/modules/main/BuilderView.tsx @@ -1,102 +1,192 @@ -import { Box, Button, Stack, Typography } from '@mui/material'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ConversationsViewIcon from '@mui/icons-material/Chat'; +import ExchangesViewIcon from '@mui/icons-material/ChatBubble'; +import SaveIcon from '@mui/icons-material/Save'; +import ChatViewIcon from '@mui/icons-material/SettingsApplications'; +import AssistantViewIcon from '@mui/icons-material/SmartToy'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import { Box, Button, Stack, Tab } from '@mui/material'; import { useLocalContext } from '@graasp/apps-query-client'; +import { Member } from '@graasp/sdk'; + +import { isEqual } from 'lodash'; -import { hooks, mutations } from '@/config/queryClient'; +import { + AssistantsSettingsType, + ChatSettingsType, + ExchangesSettingsType, +} from '@/config/appSettings'; +import { placeholderMember } from '@/config/config'; import { BUILDER_VIEW_CY } from '@/config/selectors'; +import Conversations from '@/results/ConversationsView'; -const AppSettingsDisplay = (): JSX.Element => { - const { data: appSettings } = hooks.useAppSettings(); - return ( - - App Setting - {appSettings ? ( -
{JSON.stringify(appSettings, null, 2)}
- ) : ( - Loading - )} -
- ); -}; +import AssistantsSettingsComponent from '../../settings/AssistantSettings'; +import ChatSettingsComponent from '../../settings/ChatSettings'; +import ExchangesSettingsComponent from '../../settings/ExchangesSettings'; +import { useSettings } from '../context/SettingsContext'; -const AppActionsDisplay = (): JSX.Element => { - const { data: appActions } = hooks.useAppActions(); - return ( - - App Actions - {appActions ? ( -
{JSON.stringify(appActions, null, 2)}
- ) : ( - Loading - )} -
- ); -}; +// Enum to manage tab values +enum Tabs { + ASSISTANT_VIEW = 'ASSISTANT_VIEW', + CHAT_VIEW = 'CHAT_VIEW', + EXCHANGES_VIEW = 'EXCHANGES_VIEW', + CONVERSATIONS_VIEW = 'CONVERSATIONS_VIEW', +} +// Main component: BuilderView const BuilderView = (): JSX.Element => { const { permission } = useLocalContext(); - const { data: appDatas } = hooks.useAppData(); - const { mutate: postAppData } = mutations.usePostAppData(); - const { mutate: postAppAction } = mutations.usePostAppAction(); - const { mutate: patchAppData } = mutations.usePatchAppData(); - const { mutate: deleteAppData } = mutations.useDeleteAppData(); - const { mutate: postAppSetting } = mutations.usePostAppSetting(); + + // Destructuring saved settings and save function from the custom useSettings hook + const { + assistants: assistantsSavedState, + chat: chatSavedState, + exchanges: exchangesSavedState, + saveSettings, + } = useSettings(); + + // State to manage the current values of assistants, chat, and exchanges settings + const [assistants, setAssistants] = + useState(assistantsSavedState); + const [chat, setChat] = useState(chatSavedState); + const [exchanges, setExchanges] = + useState(exchangesSavedState); + + useEffect(() => setAssistants(assistantsSavedState), [assistantsSavedState]); + useEffect(() => setChat(chatSavedState), [chatSavedState]); + useEffect(() => setExchanges(exchangesSavedState), [exchangesSavedState]); + + // Hook for translations + const { t } = useTranslation(); + + const [checkedOutMember, setCheckedOutMember] = + useState(placeholderMember); + + // State to manage the active tab, initially set to the Assistant view + const [activeTab, setActiveTab] = useState(Tabs.ASSISTANT_VIEW); return ( -
- Builder as {permission} - - - - - - - - - - App Data -
{JSON.stringify(appDatas, null, 2)}
-
- - -
+
+ + + + setActiveTab(newTab)} // Update the active tab when a new tab is selected + centered + > + } + iconPosition="start" + /> + } + iconPosition="start" + /> + } + iconPosition="start" + /> + + setActiveTab(newTab)} // Update the active tab when a new tab is selected + centered + > + } + iconPosition="start" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
); }; diff --git a/src/modules/main/PlayerView.tsx b/src/modules/main/PlayerView.tsx index 5bcc98f..78c4eed 100644 --- a/src/modules/main/PlayerView.tsx +++ b/src/modules/main/PlayerView.tsx @@ -1,22 +1,10 @@ -// import { Typography } from '@mui/material'; -// import { useLocalContext } from '@graasp/apps-query-client'; -// import { hooks } from '@/config/queryClient'; import { Box } from '@mui/material'; import { PLAYER_VIEW_CY } from '@/config/selectors'; import ParticipantInteraction from '@/modules/interaction/ParticipantInteraction'; const PlayerView = (): JSX.Element => ( - // const { permission } = useLocalContext(); - // const { data: appContext } = hooks.useAppContext(); - // const members = appContext?.members; - - {/* Player as {permission} */} - {/* */} - {/* Members */} - {/*
{JSON.stringify(members, null, 2)}
*/} - {/*
*/}
); diff --git a/src/modules/message/MessageInput.tsx b/src/modules/message/MessageInput.tsx index cf1a45e..a1bc843 100644 --- a/src/modules/message/MessageInput.tsx +++ b/src/modules/message/MessageInput.tsx @@ -1,4 +1,5 @@ import { ReactElement, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import CheckIcon from '@mui/icons-material/CheckRounded'; import SendRoundedIcon from '@mui/icons-material/SendRounded'; @@ -8,50 +9,36 @@ import FormControl from '@mui/material/FormControl'; import Textarea from '@mui/material/OutlinedInput'; import Stack from '@mui/material/Stack'; -import Exchange from '@/types/Exchange'; - export type MessageInputProps = { - exchange: Exchange; - textAreaValue: string; - setTextAreaValue: (value: string) => void; - onSubmit: (keyPressData: KeyPressData[]) => void; - completed: boolean; - setExchange: (exchange: Exchange) => void; - goToNextExchange: () => void; -}; - -type KeyPressData = { - timestamp: number; - key: string; + dismissExchange: () => void; + onSubmit: ({ content }: { content: string }) => void; + exchangeCompleted: boolean; }; +// Main component function: MessageInput const MessageInput = ({ - exchange, - textAreaValue, - setTextAreaValue, + dismissExchange, onSubmit, - completed, - setExchange, - goToNextExchange, + exchangeCompleted, }: MessageInputProps): ReactElement => { + // State to manage the value of the textarea input + const [textAreaValue, setTextAreaValue] = useState(''); + + // Ref to get direct access to the textarea DOM element const textAreaRef = useRef(null); - const [keypressData, setKeypressData] = useState([]); - function dismissExchange(): void { - const updatedExchange = { ...exchange }; - updatedExchange.dismissed = true; - updatedExchange.dismissedAt = new Date(); - setExchange(updatedExchange); - goToNextExchange(); - } + // Hook for internationalization (i18n) translation + const { t } = useTranslation(); - // const focusOnTextArea = (): void => { - // const textareaElement = textAreaRef?.current?.querySelector('textarea'); - // if (textareaElement) { - // textareaElement.focus(); - // } - // }; + // Function to focus on the textarea input + const focusOnTextArea = (): void => { + const textareaElement = textAreaRef?.current?.querySelector('textarea'); + if (textareaElement) { + textareaElement.focus(); + } + }; + // Function to remove focus from the textarea input const blurTextArea = (): void => { const textareaElement = textAreaRef?.current?.querySelector('textarea'); if (textareaElement) { @@ -59,22 +46,24 @@ const MessageInput = ({ } }; + // Effect to focus on the textarea whenever the component renders useEffect(() => { - // focusOnTextArea(); + focusOnTextArea(); }); + // Function to handle the send button click const handleClick = (): void => { if (textAreaValue.trim() !== '') { - onSubmit(keypressData); + onSubmit({ content: textAreaValue }); + setTextAreaValue(''); - // focus on the text area - // focusOnTextArea(); - // blue text area + focusOnTextArea(); blurTextArea(); } }; + // Function to handle the dismiss button click const handleDismiss = (): void => { dismissExchange(); }; @@ -83,7 +72,7 @@ const MessageInput = ({