From 33f438aa5b03b0e67cfa4584e6c8ae99e102b614 Mon Sep 17 00:00:00 2001 From: Juan Carlos Farah Date: Tue, 2 May 2023 19:17:26 +0200 Subject: [PATCH] feat: first working version --- .editorconfig | 8 + package.json | 29 +- public/favicon.ico | Bin 0 -> 3870 bytes public/logo192.png | Bin 0 -> 5347 bytes public/logo512.png | Bin 0 -> 9664 bytes public/manifest.json | 25 + src/config/appActionsTypes.ts | 20 + src/config/appDataTypes.ts | 20 + src/config/appSettingsTypes.ts | 26 + src/config/constants.ts | 50 + src/config/env.ts | 3 + src/config/layout.ts | 11 + src/config/selectors.ts | 176 +- src/config/settings.ts | 73 + src/constants/messages.ts | 2 + src/hooks/useChatbotApi.tsx | 66 + src/interfaces/comment.ts | 22 + src/interfaces/reportedComment.ts | 9 + src/interfaces/settings.ts | 68 + src/langs/en.json | 152 +- src/main.tsx | 4 +- src/mocks/db.ts | 84 +- src/modules/common/ChatbotAvatar.tsx | 16 + src/modules/common/ChatbotPrompt.tsx | 162 ++ src/modules/common/CodeEditor.tsx | 48 + src/modules/common/Comment.tsx | 132 + src/modules/common/CommentActions.tsx | 114 + src/modules/common/CommentBody.tsx | 133 + src/modules/common/CommentEditor.tsx | 201 ++ src/modules/common/CommentThread.tsx | 218 ++ src/modules/common/ReportCommentDialog.tsx | 77 + src/modules/common/ResponseBox.tsx | 35 + src/modules/common/TableView.tsx | 166 ++ src/modules/context/AppDataContext.tsx | 3 +- src/modules/context/CommentContext.tsx | 20 + .../context/LoadingIndicatorContext.tsx | 43 + src/modules/context/ReviewContext.tsx | 71 + src/modules/context/SettingsContext.tsx | 52 +- src/modules/layout/CommentContainer.tsx | 12 + src/modules/layout/CustomAvatar.tsx | 49 + src/modules/layout/CustomCommentCard.tsx | 8 + src/modules/layout/CustomDialog.tsx | 90 + src/modules/layout/ResponseContainer.tsx | 8 + src/modules/layout/ToolbarButton.tsx | 43 + src/modules/main/AdminView.tsx | 68 + src/modules/main/BuilderView.tsx | 31 +- src/modules/main/PlayerView.tsx | 39 +- src/modules/settings/CodeReviewSettings.tsx | 160 + src/modules/settings/DisplaySettings.tsx | 40 + src/modules/settings/DownloadActions.tsx | 48 + src/modules/settings/DownloadData.tsx | 51 + src/modules/settings/OrphanComments.tsx | 67 + src/modules/settings/SettingsFab.tsx | 123 + src/modules/settings/SettingsSwitch.tsx | 42 + src/modules/settings/SettingsView.tsx | 7 + src/modules/settings/SubmitButtons.tsx | 49 + src/utils/comments.ts | 100 + src/utils/datetime.ts | 28 + src/utils/toast.ts | 43 + src/utils/utils.ts | 15 + yarn.lock | 2572 +++++++++++++---- 61 files changed, 5427 insertions(+), 605 deletions(-) create mode 100644 .editorconfig create mode 100644 public/favicon.ico create mode 100644 public/logo192.png create mode 100644 public/logo512.png create mode 100644 public/manifest.json create mode 100644 src/config/appActionsTypes.ts create mode 100644 src/config/appDataTypes.ts create mode 100644 src/config/appSettingsTypes.ts create mode 100644 src/config/constants.ts create mode 100644 src/config/layout.ts create mode 100644 src/config/settings.ts create mode 100644 src/constants/messages.ts create mode 100644 src/hooks/useChatbotApi.tsx create mode 100644 src/interfaces/comment.ts create mode 100644 src/interfaces/reportedComment.ts create mode 100644 src/interfaces/settings.ts create mode 100644 src/modules/common/ChatbotAvatar.tsx create mode 100644 src/modules/common/ChatbotPrompt.tsx create mode 100644 src/modules/common/CodeEditor.tsx create mode 100644 src/modules/common/Comment.tsx create mode 100644 src/modules/common/CommentActions.tsx create mode 100644 src/modules/common/CommentBody.tsx create mode 100644 src/modules/common/CommentEditor.tsx create mode 100644 src/modules/common/CommentThread.tsx create mode 100644 src/modules/common/ReportCommentDialog.tsx create mode 100644 src/modules/common/ResponseBox.tsx create mode 100644 src/modules/common/TableView.tsx create mode 100644 src/modules/context/CommentContext.tsx create mode 100644 src/modules/context/LoadingIndicatorContext.tsx create mode 100644 src/modules/context/ReviewContext.tsx create mode 100644 src/modules/layout/CommentContainer.tsx create mode 100644 src/modules/layout/CustomAvatar.tsx create mode 100644 src/modules/layout/CustomCommentCard.tsx create mode 100644 src/modules/layout/CustomDialog.tsx create mode 100644 src/modules/layout/ResponseContainer.tsx create mode 100644 src/modules/layout/ToolbarButton.tsx create mode 100644 src/modules/main/AdminView.tsx create mode 100644 src/modules/settings/CodeReviewSettings.tsx create mode 100644 src/modules/settings/DisplaySettings.tsx create mode 100644 src/modules/settings/DownloadActions.tsx create mode 100644 src/modules/settings/DownloadData.tsx create mode 100644 src/modules/settings/OrphanComments.tsx create mode 100644 src/modules/settings/SettingsFab.tsx create mode 100644 src/modules/settings/SettingsSwitch.tsx create mode 100644 src/modules/settings/SettingsView.tsx create mode 100644 src/modules/settings/SubmitButtons.tsx create mode 100644 src/utils/comments.ts create mode 100644 src/utils/datetime.ts create mode 100644 src/utils/toast.ts create mode 100644 src/utils/utils.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..78c6dde --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/package.json b/package.json index 22bca2c..0aec2b1 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,20 @@ { - "name": "graasp-app-starter-ts-vite", - "version": "0.0.0", + "name": "graasp-app-chatbot", + "version": "0.1.0", "license": "AGPL-3.0-only", "author": "Graasp", "contributors": [ + "Juan Carlos Farah", "Basile Spaenlehauer" ], "homepage": ".", "dependencies": { + "@codemirror/lang-java": "6.0.1", + "@codemirror/lang-javascript": "6.1.7", + "@codemirror/lang-python": "6.1.2", "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", - "@graasp/apps-query-client": "github:graasp/graasp-apps-query-client#77-move-to-vite-for-bundling", + "@graasp/apps-query-client": "github:graasp/graasp-apps-query-client#79-persist", "@graasp/sdk": "0.11.0", "@graasp/ui": "2.4.1", "@mui/icons-material": "5.11.16", @@ -19,18 +23,30 @@ "@sentry/browser": "7.44.2", "@sentry/react": "7.44.2", "@sentry/tracing": "7.44.2", - "@tanstack/react-query": "^4.28.0", - "@tanstack/react-query-devtools": "^4.28.0", + "@tanstack/react-query": "4.29.5", + "@tanstack/react-query-devtools": "4.29.5", + "@types/file-saver": "2.0.5", "@types/node": "17.0.45", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", + "@uiw/react-codemirror": "4.19.16", + "date-fns": "2.29.3", + "file-saver": "2.0.5", "i18next": "22.4.13", "immutable": "4.3.0", + "lodash.isobject": "3.0.2", + "lodash.isstring": "4.0.1", + "prism-react-renderer": "^2.0.3", "qs": "6.11.1", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "12.2.0", + "react-markdown": "8.0.7", + "react-mde": "12.0.8", + "react-router-dom": "^6.10.0", "react-toastify": "9.1.2", + "remark-breaks": "^3.0.2", + "remark-gfm": "^3.0.1", "typescript": "4.9.5" }, "scripts": { @@ -58,6 +74,9 @@ "@cypress/code-coverage": "3.10.4", "@trivago/prettier-plugin-sort-imports": "4.1.1", "@types/i18n": "0.13.6", + "@types/lodash.isequal": "^4.5.6", + "@types/lodash.isobject": "^3.0.7", + "@types/lodash.isstring": "^4.0.7", "@types/uuid": "9.0.1", "@typescript-eslint/eslint-plugin": "5.56.0", "@typescript-eslint/parser": "5.56.0", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..da6a72a --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Chatbot", + "name": "Graasp Chatbot", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/src/config/appActionsTypes.ts b/src/config/appActionsTypes.ts new file mode 100644 index 0000000..f14a3a9 --- /dev/null +++ b/src/config/appActionsTypes.ts @@ -0,0 +1,20 @@ +export enum APP_ACTIONS_TYPES { + RUN_CODE = 'run_code', + SAVE_CODE = 'save_code', + CLEAR_OUTPUT = 'clear_output', + OPEN_FIGURE = 'open_figure', + STOP_EXECUTION_DURING_PROMPT = 'stop_execution_during_prompt', + STOP_EXECUTION = 'stop_execution', + SUBMITTED_INPUT = 'submitted_input', + CANCEL_PROMPT = 'cancel_prompt', + INITIALIZE_EXECUTION = 'initialize_execution', + NEW_FIGURE = 'new_figure', + SEND_PROMPT = 'send_prompt', + + // Review actions + CREATE_COMMENT = 'create_comment', + EDIT_COMMENT = 'edit_comment', + REPORT_COMMENT = 'report_comment', + DELETE_COMMENT = 'delete_comment', + RESPOND_COMMENT = 'respond_comment', +} diff --git a/src/config/appDataTypes.ts b/src/config/appDataTypes.ts new file mode 100644 index 0000000..bda85d4 --- /dev/null +++ b/src/config/appDataTypes.ts @@ -0,0 +1,20 @@ +enum APP_DATA_TYPES { + COMMENT = 'comment', + TEACHER_COMMENT = 'teacher_comment', + BOT_COMMENT = 'bot_comment', + CODE = 'code', + FLAG = 'flag', +} + +enum APP_DATA_VISIBILITY { + MEMBER = 'member', + ITEM = 'item', +} + +export const COMMENT_APP_DATA_TYPES: string[] = [ + APP_DATA_TYPES.COMMENT, + APP_DATA_TYPES.TEACHER_COMMENT, + APP_DATA_TYPES.BOT_COMMENT, +]; + +export { APP_DATA_TYPES, APP_DATA_VISIBILITY }; diff --git a/src/config/appSettingsTypes.ts b/src/config/appSettingsTypes.ts new file mode 100644 index 0000000..13ce044 --- /dev/null +++ b/src/config/appSettingsTypes.ts @@ -0,0 +1,26 @@ +export const GENERAL_SETTINGS_NAME = 'GENERAL_SETTINGS'; +export const CODE_EXECUTION_SETTINGS_NAME = 'CODE_EXECUTION_SETTINGS'; +export const CHATBOT_PROMPT_SETTINGS_NAME = 'CHATBOT_PROMPT_SETTINGS'; +export const DIFF_VIEW_SETTINGS_NAME = 'DIFF_VIEW_SETTINGS'; +export const APP_MODE_SETTINGS_NAME = 'APP_MODE_SETTINGS'; +export const DATA_FILE_LIST_SETTINGS_NAME = 'DATA_FILE_LIST_SETTINGS'; +export const INSTRUCTOR_CODE_VERSION_SETTINGS_NAME = 'INSTRUCTOR_CODE_VERSION'; +export const DATA_FILE_SETTINGS_NAME = 'DATA_FILE_SETTING'; + +export enum CodeEditorSubmitTarget { + Settings, + Code, +} + +export enum AppMode { + Execute = 'Execute', + Review = 'Review', + Collaborate = 'Collaborate', + Explain = 'Explain', +} + +export type DataFile = { + appSettingId: string; + fileName: string; + virtualPath: string; +}; diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..7e71ebf --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,50 @@ +export const GRAASP_LOGO_HEADER_HEIGHT = 40; +export const BUTTON_LOADER_SIZE = 24; + +// Admin view constants +export const NB_COL_TABLE_VIEW_TABLE = 4; + +// reset timeout in ms for settings dialog +export const CLOSE_SETTINGS_TIMEOUT = 500; + +// commit message truncation length +export const DEFAULT_TRUNCATION_COMMIT_MESSAGE_LENGTH = 15; + +export const MAX_INITIALS_AVATAR = 2; + +export const DEFAULT_REPL_INPUT_VALUE = ''; + +export const ANONYMOUS_USER = 'You'; +export const INSTRUCTOR_CODE_NAME = 'Default'; + +export const INSTRUCTOR_CODE_ID = 'instructor'; + +export const JAVASCRIPT = 'javascript'; +export const JAVA = 'java'; +export const PYTHON = 'python'; +export const MATLAB = 'matlab'; +export const JSON_LANG = 'json'; + +export const REVIEW_MODE_INDIVIDUAL = 'individual'; +export const REVIEW_MODE_COLLABORATIVE = 'collaborative'; + +export const VISIBILITY_MEMBER = 'member'; +export const VISIBILITY_ITEM = 'item'; + +export const REVIEW_MODES = [ + { + label: 'Individual - Each student works in isolation', + value: REVIEW_MODE_INDIVIDUAL, + }, + { + label: 'Collaborative - Students see other students work', + value: REVIEW_MODE_COLLABORATIVE, + }, +] as const; + +export const DEFAULT_BOT_USERNAME = 'Graasp Bot'; + +// maximum number of messages allowed in a chatbot thread +export const MAX_CHATBOT_THREAD_LENGTH = 50; + +export const DEFAULT_CHATBOT_PROMPT_APP_DATA = { chatbotPromptSettingId: '' }; diff --git a/src/config/env.ts b/src/config/env.ts index 1197aca..386d3ef 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -6,6 +6,7 @@ const { VITE_GA_MEASUREMENT_ID, VITE_MOCK_API, VITE_API_HOST, + VITE_OPEN_AI_API_URL, } = window.Cypress ? Cypress.env() : import.meta.env; export const MOCK_API = VITE_MOCK_API === 'true'; @@ -15,3 +16,5 @@ export const VERSION = VITE_VERSION || 'latest'; export const GRAASP_APP_ID = VITE_GRAASP_APP_ID; export const SENTRY_ENV = VITE_SENTRY_ENV; export const SENTRY_DSN = VITE_SENTRY_DSN; + +export const OPEN_AI_API_URL = VITE_OPEN_AI_API_URL; diff --git a/src/config/layout.ts b/src/config/layout.ts new file mode 100644 index 0000000..f5ff46e --- /dev/null +++ b/src/config/layout.ts @@ -0,0 +1,11 @@ +export enum AppView { + LoadingView, + CodeReview, + CodeEditor, + CodeExecution, + DiffView, +} + +export const SMALL_BORDER_RADIUS = '4px'; +export const BIG_BORDER_RADIUS = '8px'; +export const MAX_REPL_HEIGHT = '60vh'; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 3d48de7..b83f722 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -1,6 +1,180 @@ +export const GRAASP_LOGO_CYPRESS = 'graasp_logo'; +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 PRESET_VIEW_PANE_CYPRESS = 'preset_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 ANALYTICS_VIEW_CY = 'analytics_view'; +export const TAB_PRESET_VIEW_CYPRESS = 'tab_preset_view'; +export const TAB_TABLE_VIEW_CYPRESS = 'tab_table_view'; +export const TAB_SETTINGS_VIEW_CYPRESS = 'tab_settings_view'; +export const TABLE_ROW_USERS_CYPRESS = 'table_row_users'; +export const TABLE_VIEW_BODY_USERS_CYPRESS = 'table_view_body_users'; +export const TABLE_VIEW_OPEN_REVIEW_BUTTON_CYPRESS = + 'table_view_open_review_button'; +export const TABLE_VIEW_USER_REVIEW_DIALOG_CYPRESS = + 'table_view_user_review_dialog'; +export const TABLE_VIEW_USERNAME_CELL_CYPRESS = 'table_view_username_cell'; +export const TABLE_VIEW_HELP_NEEDED_CELL_CYPRESS = + 'table_view_help_needed_cell'; +export const TABLE_VIEW_NB_COMMENTS_CELL_CYPRESS = + 'table_view_nb_comments_cell'; +export const TABLE_VIEW_VIEW_COMMENTS_CELL_CYPRESS = + 'table_view_view_comments_cell'; +export const TABLE_VIEW_REVIEW_DIALOG_CLOSE_BUTTON_CYPRESS = + 'table_view_review_dialog_close_button'; + +export const CUSTOM_DIALOG_TITLE_CYPRESS = 'custom_dialog_title'; +export const CUSTOM_DIALOG_ACTIONS_CYPRESS = 'custom_dialog_actions'; +export const CUSTOM_DIALOG_CONTENT_CY = 'custom_dialog_content'; + +export const NUMBER_OF_COMMENTS_CYPRESS = 'number_of_comments'; +export const TABLE_NO_COMMENTS_CYPRESS = 'table_no_comments'; +export const SETTINGS_SPEED_FAB_CYPRESS = 'settings_speed_fab'; +export const DISPLAY_SETTINGS_FAB_CYPRESS = 'display_settings_fab'; +export const CODE_SETTINGS_FAB_CYPRESS = 'code_settings_fab'; +export const SETTINGS_DIALOG_CANCEL_BUTTON_CYPRESS = + 'settings_dialog_cancel_button'; +export const SETTINGS_DIALOG_SAVE_BUTTON_CYPRESS = + 'settings_dialog_save_button'; +export const PROGRAMMING_LANGUAGE_SELECT_ID = 'programmingLanguageSelect'; +export const PROGRAMMING_LANGUAGE_SELECT_CYPRESS = + 'programming_language_select'; +export const SETTINGS_CODE_DIALOG_WINDOW_CYPRESS = + 'settings_code_dialog_window'; +export const SETTINGS_DISPLAY_DIALOG_WINDOW_CYPRESS = + 'settings_display_dialog_window'; +export const SHOW_HEADER_SWITCH_CYPRESS = 'show_header_switch'; +export const SHOW_TOOLBAR_SWITCH_CYPRESS = 'show_toolbar_switch'; +export const SHOW_VERSION_NAVIGATION_SWITCH_CYPRESS = + 'show_version_navigation_switch'; +export const SHOW_EDIT_BUTTON_SWITCH_CYPRESS = 'show_edit_button_switch'; +export const SHOW_RUN_BUTTON_SWITCH_CYPRESS = 'show_run_button_switch'; +export const SHOW_VISIBILITY_SWITCH_CYPRESS = 'show_visibility_switch'; +export const ALLOW_COMMENTS_SWITCH_CYPRESS = 'allow_comments_switch'; +export const ALLOW_REPLIES_SWITCH_CYPRESS = 'allow_replies_switch'; +export const REVIEW_MODES_SELECT_CYPRESS = 'review_modes_select'; + +export const tableRowUserCypress = (id: string): string => + `${TABLE_ROW_USERS_CYPRESS}-${id}`; +export const CODE_REVIEW_CONTAINER_CYPRESS = 'code_review_container'; +export const CODE_REVIEW_ADD_BUTTON_CYPRESS = 'code_review_add_button'; +export const CODE_REVIEW_LINE_CYPRESS = 'code_review_line'; +export const CODE_REVIEW_LINE_CONTENT_CYPRESS = 'code_review_line_content'; +export const buildAddButtonDataCy = (index: number): string => + `${CODE_REVIEW_ADD_BUTTON_CYPRESS}-${index}`; + +export const CODE_REVIEW_CHATBOT_PROMPT_COMMENT_CY = + 'code_review_chatbot_prompt_comment'; + +export const COMMENT_EDITOR_CYPRESS = 'comment_editor'; +export const COMMENT_EDITOR_CANCEL_BUTTON_CYPRESS = + 'comment_editor_cancel_button'; +export const COMMENT_EDITOR_SAVE_BUTTON_CYPRESS = 'comment_editor_save_button'; +export const COMMENT_EDITOR_BOLD_BUTTON_CYPRESS = 'comment_editor_bold_button'; +export const COMMENT_EDITOR_ITALIC_BUTTON_CYPRESS = + 'comment_editor_italic_button'; +export const COMMENT_EDITOR_CODE_BUTTON_CYPRESS = 'comment_editor_code_button'; +export const COMMENT_EDITOR_LINK_BUTTON_CYPRESS = 'comment_editor_link_button'; +export const COMMENT_EDITOR_QUOTE_BUTTON_CYPRESS = + 'comment_editor_quote_button'; +export const COMMENT_EDITOR_LINE_INFO_TEXT_CYPRESS = + 'comment_editor_line_info_text'; +export const COMMENT_EDITOR_TEXTAREA_CYPRESS = 'comment_editor_textarea'; +export const COMMENT_EDITOR_TEXTAREA_HELPER_TEXT_CY = + 'comment_editor_textarea_helper_text'; +export const CODE_REVIEW_TOOLBAR_CYPRESS = 'comment_review_toolbar'; +export const TOOLBAR_USER_SELECT_CYPRESS = 'toolbar_user_select'; +export const TOOLBAR_VERSION_SELECT_CYPRESS = 'toolbar_version_select'; +export const TOOLBAR_COMMIT_INFO_BUTTON_CYPRESS = 'toolbar_commit_info'; +export const TOOLBAR_EDIT_CODE_BUTTON_CYPRESS = 'toolbar_edit_code_button'; +export const TOOLBAR_VISIBILITY_BUTTON_CYPRESS = 'toolbar_visibility_button'; +export const TOOLBAR_RUN_CODE_BUTTON_CYPRESS = 'toolbar_run_code_button'; +export const COMMENT_CONTAINER_CYPRESS = 'comment_container'; +export const CHATBOT_PROMPT_CONTAINER_CY = 'chatbot_prompt_container'; +export const COMMENT_RESPONSE_BOX_CY = 'comment-response-box'; +export const buildCommentContainerDataCy = (id: string): string => + `${COMMENT_CONTAINER_CYPRESS}-${id}`; +export const buildChatbotPromptContainerDataCy = (id: string): string => + `${CHATBOT_PROMPT_CONTAINER_CY}-${id}`; +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'; +export const CODE_EDITOR_CONTAINER_CYPRESS = 'code_editor_container'; +export const CODE_EDITOR_LANGUAGE_SELECT_CYPRESS = + 'code_editor_language_select'; +export const CODE_EDITOR_COMMIT_MESSAGE_CYPRESS = 'code_editor_commit_message'; +export const CODE_EDITOR_COMMIT_DESCRIPTION_CYPRESS = + 'code_editor_commit_description'; +export const CODE_EDITOR_SUBMIT_BUTTON_CYPRESS = 'code_editor_submit_button'; +export const CODE_EDITOR_CANCEL_BUTTON_CYPRESS = 'code_editor_cancel_button'; + +export const COMMIT_INFO_DIALOG_CYPRESS = 'commit_info_dialog'; +export const COMMIT_INFO_FIELD_CYPRESS = 'commit_info_field'; +export const buildCommitFieldDataCy = (fieldName: string): string => + `${COMMIT_INFO_FIELD_CYPRESS}-${fieldName}`; + +export const REPL_CONTAINER_CY = 'repl_container'; +export const REPL_EDITOR_ID_CY = 'repl_editor'; + +export const APP_MODE_EXECUTE_BUTTON_CY = 'app_mode_execute'; +export const APP_MODE_REVIEW_BUTTON_CY = 'app_mode_review'; +export const APP_MODE_COLLABORATE_BUTTON_CY = 'app_mode_collaborate'; +export const APP_MODE_EXPLAIN_BUTTON_CY = 'app_mode_explain'; + +export const SETTING_APP_MODE_SELECT_FORM_LABEL_CY = + 'setting_app_mode_select_label'; +export const SETTING_APP_MODE_SELECT_NAME_CY = 'setting_app_mode_select_label'; +export const SETTING_MAIN_CODE_EDITOR_CY = 'setting_main_code_editor'; +export const SETTING_HEADER_CODE_EDITOR_CY = 'setting_header_code_editor'; +export const SETTING_FOOTER_CODE_EDITOR_CY = 'setting_footer_code_editor'; +export const SETTING_CHATBOT_PROMPT_CODE_EDITOR_CY = + 'setting_chatbot_prompt_code_editor'; +export const SETTING_INITIAL_PROMPT_CODE_EDITOR_CY = + 'setting_initial_prompt_code_editor'; +export const SETTING_CHATBOT_PROMPT_LINE_NUMBER_CY = + 'setting_chatbot_prompt_line_number'; +export const SETTING_MAX_COMMENT_LENGTH = 'setting_max_comment_length'; +export const SETTING_ADD_CHATBOT_PROMPT_CY = 'setting_add_chatbot_prompt'; + +export const REPL_RUN_CODE_BUTTON_CY = 'repl_run_code_button'; +export const REPL_OUTPUT_CONSOLE_CY = 'repl_output_console_area'; +export const REPL_STATUS_INDICATOR_CY = 'repl_status_indicator'; +export const REPL_SAVE_BUTTON_CY = 'repl_save_button'; +export const REPL_STOP_BUTTON_CY = 'repl_stop_button'; +export const REPL_CLEAR_BUTTON_CY = 'repl_clear_button'; +export const REPL_FULL_SCREEN_BUTTON_CY = 'repl_full_screen_button'; +export const REPL_INPUT_DIALOG_BUTTON_CONTAINER_CY = 'repl_input_container'; +export const REPL_INPUT_DIALOG_CANCEL_BUTTON_CY = 'repl_input_cancel_button'; +export const REPL_INPUT_DIALOG_SUBMIT_BUTTON_CY = 'repl_input_submit_button'; +export const REPL_INPUT_DIALOG_PROMPT_TEXT_CY = 'repl_input_prompt_text'; +export const REPL_INPUT_DIALOG_TEXTFIELD_CY = 'repl_input_textfield'; + +export const DOWNLOAD_ACTIONS_BUTTON_CY = 'download_actions_button'; + +export const DOWNLOAD_DATA_BUTTON_CY = 'download_data_button'; + +export const DIFF_VIEW_CONTAINER_CY = 'diff_view_container'; +export const DIFF_VIEW_COMPONENT_CY = 'diff_view_component'; + +export const SETTING_DIFF_VIEW_OLD_CODE_CY = 'setting_diff_view_old_code'; +export const SETTING_DIFF_VIEW_NEW_CODE_CY = 'setting_diff_view_new_code'; export const buildDataCy = (selector: string): string => `[data-cy=${selector}]`; +export const buildTableRowCypress = (selector: string): string => + `[data-cy=${selector}]`; +export const buildCommitFieldCypress = (selector: string): string => + `[data-cy=${selector}]`; +export const settingKeyDataCy = (key: string): string => `setting-${key}`; + +// keys for save buttons and tests +export const EXECUTION_MODE_SETTINGS_KEY = 'EXECUTION_MODE_SETTINGS_KEY'; +export const REVIEW_MODE_SETTINGS_KEY = 'REVIEW_MODE_SETTINGS_KEY'; +export const EXPLAIN_MODE_SETTINGS_KEY = 'EXPLAIN_MODE_SETTINGS_KEY'; +export const SETTING_NEW_CHATBOT_PROMPT_KEY = 'NEW_CHATBOT_PROMPT_KEY'; diff --git a/src/config/settings.ts b/src/config/settings.ts new file mode 100644 index 0000000..94e77ad --- /dev/null +++ b/src/config/settings.ts @@ -0,0 +1,73 @@ +import { Context, PermissionLevel } from '@graasp/sdk'; + +import { + AppModeSettings, + AppModeSettingsKeys, + ChatCompletionMessage, + ChatbotPromptSettings, + ChatbotPromptSettingsKeys, + DataFileListSettings, + DataFileListSettingsKeys, + GeneralSettings, + GeneralSettingsKeys, +} from '@/interfaces/settings'; + +import { AppMode } from './appSettingsTypes'; +import { PYTHON, REVIEW_MODE_INDIVIDUAL } from './constants'; +import { AppView } from './layout'; + +export const DEFAULT_APP_MODE = AppMode.Execute; +export const DEFAULT_APP_VIEW = AppView.CodeReview; + +export const DEFAULT_CONTEXT = Context.BUILDER; +export const DEFAULT_PERMISSION = PermissionLevel.Read; +export const DEFAULT_LINE_HIDDEN_STATE = false; + +export const DEFAULT_CONTEXT_API_HOST = ''; +export const DEFAULT_CONTEXT_ITEM_ID = ''; +export const DEFAULT_CONTEXT_LANGUAGE = 'en'; +export const DEFAULT_CONTEXT_STANDALONE = false; +export const DEFAULT_CONTEXT_OFFLINE = false; +export const DEFAULT_CONTEXT_DEV = false; +export const DEFAULT_CONTEXT_SETTINGS = {}; + +// default values +export const DEFAULT_SHOW_HEADER_SETTING = false; +export const DEFAULT_SHOW_TOOLBAR_SETTING = true; +export const DEFAULT_SHOW_VERSION_NAVIGATION_SETTING = false; +export const DEFAULT_SHOW_EDIT_BUTTON_SETTING = false; +export const DEFAULT_SHOW_RUN_BUTTON_SETTING = false; +export const DEFAULT_SHOW_VISIBILITY_BUTTON_SETTING = true; +export const DEFAULT_ALLOW_COMMENTS_SETTING = true; +export const DEFAULT_ALLOW_REPLIES_SETTING = true; +export const DEFAULT_ALLOW_COMMENT_REPORTING = true; +export const DEFAULT_PROGRAMMING_LANGUAGE_SETTING = PYTHON; +export const DEFAULT_CODE_SETTING = ''; +export const DEFAULT_COMMIT_MESSAGE_SETTING = ''; +export const DEFAULT_COMMIT_DESCRIPTION_SETTING = ''; +export const DEFAULT_MAX_COMMENT_LENGTH_SETTING = 300; + +export const DEFAULT_REVIEW_MODE_SETTING = REVIEW_MODE_INDIVIDUAL; + +// default settings object +export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = { + [GeneralSettingsKeys.AllowReplies]: DEFAULT_ALLOW_REPLIES_SETTING, + [GeneralSettingsKeys.AllowCommentsReporting]: DEFAULT_ALLOW_COMMENT_REPORTING, + [GeneralSettingsKeys.MaxCommentLength]: DEFAULT_MAX_COMMENT_LENGTH_SETTING, +}; + +// app mode setting +export const DEFAULT_APP_MODE_SETTINGS: AppModeSettings = { + [AppModeSettingsKeys.Mode]: DEFAULT_APP_MODE, +}; + +// app mode setting +export const DEFAULT_DATA_FILE_LIST_SETTINGS: DataFileListSettings = { + [DataFileListSettingsKeys.Files]: [], +}; + +// chatbot prompt setting +export const DEFAULT_CHATBOT_PROMPT_SETTINGS: ChatbotPromptSettings = { + [ChatbotPromptSettingsKeys.InitialPrompt]: [] as ChatCompletionMessage[], + [ChatbotPromptSettingsKeys.ChatbotPrompt]: '', +}; diff --git a/src/constants/messages.ts b/src/constants/messages.ts new file mode 100644 index 0000000..b752742 --- /dev/null +++ b/src/constants/messages.ts @@ -0,0 +1,2 @@ +export const UNEXPECTED_ERROR_MESSAGE = 'An unexpected error has occurred.'; +export const SUCCESS_MESSAGE = 'The operation succeeded.'; diff --git a/src/hooks/useChatbotApi.tsx b/src/hooks/useChatbotApi.tsx new file mode 100644 index 0000000..8f726e0 --- /dev/null +++ b/src/hooks/useChatbotApi.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react'; + +import { OPEN_AI_API_URL } from '@/config/env'; +import { ChatCompletionMessage } from '@/interfaces/settings'; + +export type UserDataType = { [key: string]: unknown }; +type CallbackType = ( + completion: ChatCompletionMessage, + data: UserDataType, +) => void; +type ReturnType = { + isLoading: boolean; + callApi: (prompt: ChatCompletionMessage[], userData: UserDataType) => void; +}; +export const useChatbotApi = (callback: CallbackType): ReturnType => { + const [isLoading, setIsLoading] = useState(false); + const [shouldFetch, setShouldFetch] = useState(false); + const [prompt, setPrompt] = useState([]); + const [data, setData] = useState({}); + + useEffect( + () => { + async function fetchApi(): Promise { + setIsLoading(true); + fetch(OPEN_AI_API_URL, { + method: 'POST', + body: JSON.stringify({ prompt }), + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + }) + .then((response) => { + if (response.ok) { + return response.json(); + } + return { completion: 'Sorry, an error occured' }; + }) + .then((json) => { + const { completion } = json; + callback(completion, data); + setIsLoading(false); + setShouldFetch(false); + }); + } + if (prompt && shouldFetch) { + fetchApi(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [shouldFetch], + ); + + const callApi = ( + chatbotPrompt: ChatCompletionMessage[], + userData: { [key: string]: unknown }, + ): void => { + setPrompt(chatbotPrompt); + setData(userData); + setShouldFetch(true); + }; + + return { + isLoading, + callApi, + }; +}; diff --git a/src/interfaces/comment.ts b/src/interfaces/comment.ts new file mode 100644 index 0000000..4f75c4b --- /dev/null +++ b/src/interfaces/comment.ts @@ -0,0 +1,22 @@ +import { AppData } from '@graasp/apps-query-client'; + +import { APP_DATA_TYPES } from '../config/appDataTypes'; + +export type VisibilityVariants = 'member' | 'item'; + +export interface CommentAppData { + data: { + line: number; + codeId: string; + content: string; + parent: string; + multiline?: { + start: number; + end: number; + }; + chatbotPromptSettingId?: string; + }; + type: APP_DATA_TYPES.COMMENT | APP_DATA_TYPES.BOT_COMMENT; + visibility?: VisibilityVariants; +} +export type CommentType = AppData & CommentAppData; diff --git a/src/interfaces/reportedComment.ts b/src/interfaces/reportedComment.ts new file mode 100644 index 0000000..56f78c3 --- /dev/null +++ b/src/interfaces/reportedComment.ts @@ -0,0 +1,9 @@ +import { AppData } from '@graasp/apps-query-client'; + +interface ReportedCommentAppData { + data: { + reason: string; + commentId: string; + }; +} +export type ReportedCommentType = AppData & ReportedCommentAppData; diff --git a/src/interfaces/settings.ts b/src/interfaces/settings.ts new file mode 100644 index 0000000..1e1a91a --- /dev/null +++ b/src/interfaces/settings.ts @@ -0,0 +1,68 @@ +import { AppSetting } from '@graasp/apps-query-client'; + +import { AppMode, DataFile } from '@/config/appSettingsTypes'; + +// general settings keys +export enum GeneralSettingsKeys { + AllowReplies = 'allowReplies', + AllowCommentsReporting = 'allowCommentReporting', + MaxCommentLength = 'maxCommentLength', +} + +// App Mode Setting keys +export enum AppModeSettingsKeys { + Mode = 'mode', +} + +// App Mode Setting keys +export enum DataFileListSettingsKeys { + Files = 'files', +} + +// Chatbot Prompt Setting keys +export enum ChatbotPromptSettingsKeys { + InitialPrompt = 'initialPrompt', + ChatbotPrompt = 'chatbotPrompt', +} + +// type of general settings +export type GeneralSettings = { + [GeneralSettingsKeys.AllowReplies]: boolean; + [GeneralSettingsKeys.AllowCommentsReporting]: boolean; + [GeneralSettingsKeys.MaxCommentLength]: number; + + // used to allow access using settings[settingKey] syntax + [key: string]: unknown; +}; + +export type AppModeSettings = { + [AppModeSettingsKeys.Mode]: AppMode; + + // used to allow access using settings[settingKey] syntax + [key: string]: unknown; +}; + +export type DataFileListSettings = { + [DataFileListSettingsKeys.Files]: DataFile[]; + + // used to allow access using settings[settingKey] syntax + [key: string]: unknown; +}; + +export type ChatCompletionMessageRoles = 'system' | 'user' | 'assistant'; + +export type ChatCompletionMessage = { + role: ChatCompletionMessageRoles; + content: string; +}; + +export type ChatbotPromptSettings = { + [ChatbotPromptSettingsKeys.InitialPrompt]: ChatCompletionMessage[]; + [ChatbotPromptSettingsKeys.ChatbotPrompt]: string; + + // used to allow access using settings[settingKey] syntax + [key: string]: unknown; +}; +export type ChatbotPromptAppSettings = AppSetting & { + data: ChatbotPromptSettings; +}; diff --git a/src/langs/en.json b/src/langs/en.json index a356c2f..8c2e067 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -1,5 +1,155 @@ { "translations": { - "Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit" + "Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit", + "Cancel": "Cancel", + "Finish": "Finish", + "Save": "Save", + "Stop": "Stop", + "Clear": "Clear", + "Saved": "Saved", + "Send": "Send", + "Reply": "Reply", + "Yes": "Yes", + "Write": "Write", + "Preview": "Preview", + "User": "User", + "Version": "Version", + "Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?", + "View the Users in the Sample Space": "View the Users in the Sample Space", + "Selected Student is:": "Selected Student is:", + "No student selected": "No student selected", + "Image Uri": "Image Uri", + "Name": "Name", + "Settings": "Settings", + "Code": "Code", + "Programming Language": "Programming Language", + "Show Header to Students": "Show Header to Students", + "Allow Comments": "Allow Comments", + "Allow Replies": "Allow Replies", + "Student Comments": "Student Comments", + "Code Review": "Code Review", + "Total Number of Messages": "Total Number of Messages", + "View Chat": "View Chat", + "Select a bot to impersonate": "Select a bot to impersonate", + "Close": "Close", + "Viewing comments from": "Viewing comments from {{user}}", + "Bot Users": "Bot Users", + "Avatar": "Avatar", + "Instructor": "Instructor", + "Actions": "Actions", + "No Bots": "No Bots", + "Add a New Bot User": "Add a New Bot User", + "Warning! You are impersonating a bot. Clear the field above to write comments as yourself.": "Warning! You are impersonating a bot. Clear the field above to write comments as yourself.", + "[DELETED]": "[DELETED]", + "Toggle Visibility": "Toggle Visibility", + "Toggle Fullscreen": "Toggle Fullscreen", + "Fullscreen": "Fullscreen", + "show more": "show more", + "Delete Bot": "Delete Bot", + "Delete All Comments for Bot": "Delete All Comments for Bot", + "Enable Auto Reply": "Enable Auto Reply", + "Enable Automatic Message Seeding": "Enable Automatic Message Seeding", + "Upload File": "Upload File", + "Bot Personality": "Bot Personality", + "Reset Defaults": "Reset Defaults", + "Add Empty Step": "Add Empty Step", + "Show Toolbar to Students": "Show Toolbar to Students", + "Show All Comments": "Show All Comments", + "Hide All Comments": "Hide All Comments", + "Code Settings": "Code Settings", + "Display Settings": "Display Settings", + "Show Version Navigation": "Show Version Navigation", + "Show Code Edit Button": "Show Code Edit Button", + "Show Code Run Button": "Show Code Run Button", + "Show Visibility Toggle": "Show Visibility Toggle", + "Allow students to see other students' code": "Allow students to see other students' code", + "Downloading": "Downloading", + "Download Actions": "Download Actions", + "Edit": "Edit", + "Commit Info": "Commit Info", + "Run": "Run", + "Author": "Author", + "Message": "Message", + "Description": "Description", + "Created": "Created", + "Commit Changes": "Commit Changes", + "Commit Message": "Commit Message", + "Optional Extended Description": "Optional Extended Description", + "Updated Code sample": "Updated Code sample", + "Add an Optional Description": "Add an Optional Description", + "You can reply with": "You can reply with", + "Quick Replies": "Quick Replies", + "Restart interaction": "Restart interaction", + "Default Version": "Default Version", + "Help needed": "Help needed", + "Table View": "Table View", + "Settings View": "Settings", + "Preset View": "Preset View", + "No Comments": "No Comments", + "Define Interaction Mode": "Define Interaction Mode", + "App Customization": "App Customization", + "Delete": "Delete", + "Report": "Report", + "Reason": "Reason", + "Report a comment": "Report a comment", + "Please provide below the reason for reporting this comment": "Please provide below the reason for reporting this comment", + "Type your comment": "Type your comment", + "Respond to this comment": "Respond to this comment", + "LineComment": "Line Comment: {{line}}", + "MultiLineComment": "Multiline Comment: {{start}} - {{end}}", + "Define Review Mode": "Define Review Mode", + "Individual - Each student works in isolation": "Individual - Each student works in isolation", + "Collaborative - Students see other students work": "Collaborative - Students see other students work", + "Remove orphans": "Remove orphans", + "Number of orphan threads": "Orphan threads: {{threads}} ({{totalComments}} total comments)", + "total comments": "total comments", + "Submit Code": "Submit Code", + "User Select": "User Select", + "This is the Code Execution panel": "This is the Code Execution panel", + "Back to Code Review": "Back to Code Review", + "No Commit Message": "~No message~", + "Execute Code": "Execute Code", + "Review Code": "Review Code", + "Collaborate on Code": "Collaborate on Code", + "Header code": "Header code", + "Footer code": "Footer code", + "Code to review": "Code to review", + "Input requested": "Input requested", + "Submit": "Submit", + "Unsaved modifications": "Unsaved modifications", + "APP_VERSION": "version {{ version }}", + "Choose the App Mode": "Choose the App Mode", + "File added": "File added", + "Upload complete": "Upload complete", + "This is just a preview. No files can be uploaded.": "This is just a preview. No files can be uploaded.", + "Data files": "Data files", + "Upload Data Files": "Upload Data Files", + "No files": "No files", + "Supported formats": "Supported formats: {{formats}}", + "Old Code": "Old Code", + "New Code": "New Code", + "Line Offset": "Line Offset", + "Save Setting to Preview": "Save Setting to Preview", + "Show Preview": "Show Preview", + "Pre-loaded libraries": "Preloaded Libraries", + "Stop Execution": "Stop Execution", + "Clear outputs and figures": "Clear Outputs and Figures", + "Run Code": "Run Code", + "Save Code": "Save Code", + "just now": "just now", + "Maximum comment length": "Maximum comment length", + "COMMENT_TEXT_TOO_LONG": "The comment text can not be longer than {{max_length}} characters", + "Add New Chatbot Prompt": "Add New Chatbot Prompt", + "Edit Chatbot Prompt": "Edit Chatbot Prompt", + "Chatbot Prompts": "Chatbot Prompts", + "No Chatbot Prompts": "No Chatbot Prompts", + "Initial Prompt": "Initial Prompt", + "Chatbot Prompt": "Chatbot Prompt", + "Line Number (first line is 0)": "Line Number (first line is 0)", + "Loading": "Loading", + "Optional Text": "Optional Text", + "Download Data": "Download Data", + "Cue": "Cue", + "Prompt": "Prompt" } } diff --git a/src/main.tsx b/src/main.tsx index 2a61418..cec1d92 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,7 +7,7 @@ import { Replay } from '@sentry/browser'; import * as Sentry from '@sentry/react'; import { BrowserTracing } from '@sentry/tracing'; -import { MOCK_API } from './config/env'; +import { MOCK_API, OPEN_AI_API_URL } from './config/env'; import { generateSentryConfig } from './config/sentry'; import './index.css'; import buildDatabase, { mockContext, mockMembers } from './mocks/db'; @@ -26,7 +26,7 @@ Sentry.init({ /* istanbul ignore next */ if (MOCK_API) { mockApi({ - externalUrls: [], + externalUrls: [OPEN_AI_API_URL], appContext: window.Cypress ? window.appContext : mockContext, database: window.Cypress ? window.database diff --git a/src/mocks/db.ts b/src/mocks/db.ts index a7b98a6..215abe2 100644 --- a/src/mocks/db.ts +++ b/src/mocks/db.ts @@ -1,5 +1,8 @@ import type { Database, LocalContext, Member } from '@graasp/apps-query-client'; +import { v4 } from 'uuid'; + +// import { APP_DATA_TYPES } from '@/config/appDataTypes'; import { API_HOST } from '@/config/env'; export const mockContext: LocalContext = { @@ -7,32 +10,91 @@ export const mockContext: LocalContext = { permission: 'admin', context: 'player', itemId: '1234-1234-123456-8123-123456', - memberId: 'mock-member-id', + memberId: v4(), }; export const mockMembers: Member[] = [ { - id: mockContext.memberId || '', - name: 'current-member', - email: '', - extra: {}, - }, - { - id: 'mock-member-id-2', - name: 'mock-member-2', + id: mockContext.memberId || v4(), + name: 'ID-123', email: '', extra: {}, }, ]; +// const commentBot = v4(); +// const commentParent = v4(); + const buildDatabase = ( appContext: Partial, members?: Member[], ): Database => ({ - appData: [], + appData: [ + // { + // id: commentBot, + // data: { + // content: 'This would be thee bot comment', + // parent: null, + // chatbotPromptSettingId: 'rubbish', + // }, + // memberId: 'mock-member-id', + // type: APP_DATA_TYPES.BOT_COMMENT, + // itemId: appContext.itemId || '', + // visibility: 'member', + // creator: 'mock-member-id', + // createdAt: new Date().toISOString(), + // updatedAt: new Date().toISOString(), + // }, + // { + // id: commentParent, + // data: { + // content: '*Hello* this is a _comment_ on line 3', + // parent: commentBot, + // }, + // memberId: 'mock-member-id', + // type: APP_DATA_TYPES.COMMENT, + // itemId: appContext.itemId || '', + // visibility: 'member', + // creator: 'mock-member-id', + // createdAt: new Date().toISOString(), + // updatedAt: new Date().toISOString(), + // }, + // { + // id: v4(), + // data: { + // content: '*Hello* this is a _response_ on line 3', + // parent: commentParent, + // }, + // memberId: 'mock-member-id', + // type: APP_DATA_TYPES.COMMENT, + // itemId: appContext.itemId || '', + // creator: 'mock-member-id', + // visibility: 'member', + // createdAt: new Date().toISOString(), + // updatedAt: new Date().toISOString(), + // }, + ], appActions: [], members: members ?? mockMembers, - appSettings: [], + appSettings: [ + // { + // id: 'e09b45ad-4391-48fd-99f1-8f0f300f4b59', + // name: 'CHATBOT_PROMPT_SETTINGS', + // itemId: '81f2dc95-1f15-48f6-92b2-913e38265270', + // data: { + // chatbotPrompt: 'Hello! I am a chatbot. Ask me anything.', + // initialPrompt: [ + // { + // role: 'system', + // content: 'You are a chatbot.', + // }, + // ], + // }, + // creator: 'a25baa07-09e6-4c8c-a53e-9adc94937037', + // createdAt: '2023-04-26T09:34:16.160Z', + // updatedAt: '2023-04-26T09:34:16.160Z', + // }, + ], }); export default buildDatabase; diff --git a/src/modules/common/ChatbotAvatar.tsx b/src/modules/common/ChatbotAvatar.tsx new file mode 100644 index 0000000..d87bcbf --- /dev/null +++ b/src/modules/common/ChatbotAvatar.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; + +import { SmartToy as BotIcon } from '@mui/icons-material'; +import { Avatar } from '@mui/material'; + +const ChatbotAvatar: FC = () => ( + + + +); + +export default ChatbotAvatar; diff --git a/src/modules/common/ChatbotPrompt.tsx b/src/modules/common/ChatbotPrompt.tsx new file mode 100644 index 0000000..84d3f2b --- /dev/null +++ b/src/modules/common/ChatbotPrompt.tsx @@ -0,0 +1,162 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { CardContent, CardHeader } from '@mui/material'; + +import { APP_ACTIONS_TYPES } from '@/config/appActionsTypes'; +import { APP_DATA_TYPES, COMMENT_APP_DATA_TYPES } from '@/config/appDataTypes'; +import { GENERAL_SETTINGS_NAME } from '@/config/appSettingsTypes'; +import { DEFAULT_BOT_USERNAME, INSTRUCTOR_CODE_ID } from '@/config/constants'; +import { MUTATION_KEYS, useMutation } from '@/config/queryClient'; +import { + buildChatbotPromptContainerDataCy, + buildCommentResponseBoxDataCy, +} from '@/config/selectors'; +import { DEFAULT_GENERAL_SETTINGS } from '@/config/settings'; +import { UserDataType, useChatbotApi } from '@/hooks/useChatbotApi'; +import { + ChatCompletionMessage, + ChatCompletionMessageRoles, + GeneralSettingsKeys, +} from '@/interfaces/settings'; + +import { useAppDataContext } from '../context/AppDataContext'; +import { useLoadingIndicator } from '../context/LoadingIndicatorContext'; +import { useSettings } from '../context/SettingsContext'; +import CommentContainer from '../layout/CommentContainer'; +import CustomCommentCard from '../layout/CustomCommentCard'; +import ChatbotAvatar from './ChatbotAvatar'; +import CommentBody from './CommentBody'; +import CommentEditor from './CommentEditor'; +import ResponseBox from './ResponseBox'; + +const ChatbotPrompt: FC = () => { + const { t } = useTranslation(); + const { postAppDataAsync, appData } = useAppDataContext(); + const [openEditor, setOpenEditor] = useState(false); + const { mutate: postAction } = useMutation< + unknown, + unknown, + { data: unknown; type: string } + >(MUTATION_KEYS.POST_APP_ACTION); + // if a message already exists with the prompt id we should not display this prompt + const { + chatbotPrompt, + [GENERAL_SETTINGS_NAME]: generalSettings = DEFAULT_GENERAL_SETTINGS, + } = useSettings(); + const { startLoading, stopLoading } = useLoadingIndicator(); + + const { callApi } = useChatbotApi( + (completion: ChatCompletionMessage, data: UserDataType) => { + const newData = { ...data, content: completion }; + // post comment from bot + postAppDataAsync({ + data: newData, + type: APP_DATA_TYPES.BOT_COMMENT, + })?.then(() => stopLoading()); + postAction({ data: newData, type: APP_ACTIONS_TYPES.CREATE_COMMENT }); + }, + ); + + const comments = appData.filter((c) => + COMMENT_APP_DATA_TYPES.includes(c.type), + ); + + const realChatbotPromptExists = comments.find( + (c) => c.data.chatbotPromptSettingId !== undefined, + ); + const handleNewDiscussion = (newUserComment: string): void => { + const chatbotMessage = chatbotPrompt?.data.chatbotPrompt; + startLoading(); + const newData = { + parent: null, + codeId: INSTRUCTOR_CODE_ID, + content: chatbotMessage, + chatbotPromptSettingId: chatbotPrompt?.id, + }; + // post chatbot comment as app data with async call + postAppDataAsync({ + data: newData, + type: APP_DATA_TYPES.BOT_COMMENT, + })?.then((botComment) => { + const userData = { + parent: botComment.id, + codeId: INSTRUCTOR_CODE_ID, + content: newUserComment, + }; + // post new user comment as appdata with normal call + postAppDataAsync({ + data: userData, + type: APP_DATA_TYPES.COMMENT, + })?.then((userMessage) => { + const fullPrompt = [ + ...(chatbotPrompt?.data.initialPrompt || []), + { + role: 'assistant' as ChatCompletionMessageRoles, + content: chatbotMessage, + }, + { + role: 'user' as ChatCompletionMessageRoles, + content: newUserComment, + }, + ]; + callApi(fullPrompt, { + parent: userMessage.id, + codeId: INSTRUCTOR_CODE_ID, + }); + postAction({ + data: { prompt: fullPrompt }, + type: APP_ACTIONS_TYPES.SEND_PROMPT, + }); + }); + postAction({ data: userData, type: APP_ACTIONS_TYPES.CREATE_COMMENT }); + }); + postAction({ data: newData, type: APP_ACTIONS_TYPES.CREATE_COMMENT }); + + // close editor + setOpenEditor(false); + }; + + // display only if real chatbot prompt does not exist yet + if (!realChatbotPromptExists) { + // console.log(chatbotPrompt); + // if (chatbotPrompt?.data?.chatbotPrompt === '') { + // return <>Please configure the chatbot prompt.; + // } + return ( + + + } + /> + + {chatbotPrompt?.data?.chatbotPrompt} + + + + {openEditor ? ( + setOpenEditor(false)} + onSend={handleNewDiscussion} + /> + ) : ( + setOpenEditor(true)} + commentId={chatbotPrompt?.id} + /> + )} + + ); + } + return null; +}; +export default ChatbotPrompt; diff --git a/src/modules/common/CodeEditor.tsx b/src/modules/common/CodeEditor.tsx new file mode 100644 index 0000000..fd13ebc --- /dev/null +++ b/src/modules/common/CodeEditor.tsx @@ -0,0 +1,48 @@ +import React, { FC } from 'react'; + +import { Box, Stack, styled, useTheme } from '@mui/material'; + +import { javascript } from '@codemirror/lang-javascript'; +import CodeMirror from '@uiw/react-codemirror'; + +import { SMALL_BORDER_RADIUS } from '@/config/layout'; +import { CODE_EDITOR_ID_CY } from '@/config/selectors'; + +const StyledEditorContainer = styled(Box)({ + border: 'solid silver 1px', + borderRadius: SMALL_BORDER_RADIUS, + maxHeight: '40vh', + overflow: 'hidden', +}); + +type Props = { + value: string; + readOnly?: boolean; + onChange?: (newValue: string) => void; +}; + +const CodeEditor: FC = ({ value, readOnly, onChange }) => { + const theme = useTheme(); + + return ( + + + + + + ); +}; + +export default CodeEditor; diff --git a/src/modules/common/Comment.tsx b/src/modules/common/Comment.tsx new file mode 100644 index 0000000..ed5661f --- /dev/null +++ b/src/modules/common/Comment.tsx @@ -0,0 +1,132 @@ +import React, { FC, ReactElement, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MoreVert } from '@mui/icons-material'; +import { + Card, + CardContent, + CardHeader, + CardProps, + IconButton, + Tooltip, + styled, +} from '@mui/material'; + +import { useLocalContext } from '@graasp/apps-query-client'; + +import { APP_DATA_TYPES } from '@/config/appDataTypes'; +import { GENERAL_SETTINGS_NAME } from '@/config/appSettingsTypes'; +import { ANONYMOUS_USER, DEFAULT_BOT_USERNAME } from '@/config/constants'; +import { BIG_BORDER_RADIUS } from '@/config/layout'; +import { MUTATION_KEYS, useMutation } from '@/config/queryClient'; +import { buildCommentContainerDataCy } from '@/config/selectors'; +import { DEFAULT_GENERAL_SETTINGS } from '@/config/settings'; +import { CommentType } from '@/interfaces/comment'; +import { ReportedCommentType } from '@/interfaces/reportedComment'; +import { GeneralSettingsKeys } from '@/interfaces/settings'; +import { getFormattedTime } from '@/utils/datetime'; + +// import { useMembersContext } from '../context/MembersContext'; +import { useSettings } from '../context/SettingsContext'; +import CustomAvatar from '../layout/CustomAvatar'; +import ChatbotAvatar from './ChatbotAvatar'; +import CommentActions from './CommentActions'; +import CommentBody from './CommentBody'; +import ReportCommentDialog from './ReportCommentDialog'; + +const CustomCard = styled(Card)({ + borderRadius: BIG_BORDER_RADIUS, +}); + +type Props = { + comment: CommentType; +}; + +const Comment: FC = ({ comment }) => { + const { t, i18n } = useTranslation(); + // const members = useMembersContext(); + const { [GENERAL_SETTINGS_NAME]: settings = DEFAULT_GENERAL_SETTINGS } = + useSettings(); + const currentMember = useLocalContext().get('memberId'); + const { mutate: postAppData } = useMutation< + ReportedCommentType, + unknown, + unknown, + ReportedCommentType + >(MUTATION_KEYS.POST_APP_DATA); + + const allowCommentReporting = + settings[GeneralSettingsKeys.AllowCommentsReporting]; + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [openActionsMenu, setOpenActionsMenu] = useState(false); + const [openFlagDialog, setOpenFlagDialog] = useState(false); + const commentRef = useRef(null); + + // currently not using members + // const member = members.find((u) => u.id === comment.memberId); + // const userName = member?.name || ANONYMOUS_USER; + + const isBot = comment.type === APP_DATA_TYPES.BOT_COMMENT; + + const isEditable = (): boolean => currentMember === comment.creator; + const isDeletable = (): boolean => isEditable(); + + const sendCommentReport = (reason: string): void => { + postAppData({ + data: { reason, commentId: comment.id }, + type: APP_DATA_TYPES.FLAG, + }); + }; + + const renderCommentActions = (): ReactElement => ( + <> + + { + setMenuAnchorEl(e.currentTarget); + setOpenActionsMenu(true); + }} + > + + + + setOpenFlagDialog(true)} + onClose={() => { + setMenuAnchorEl(null); + setOpenActionsMenu(false); + }} + /> + + + ); + return ( + + : } + action={renderCommentActions()} + /> + + {comment.data.content} + + + ); +}; + +export default Comment; diff --git a/src/modules/common/CommentActions.tsx b/src/modules/common/CommentActions.tsx new file mode 100644 index 0000000..b5f64a4 --- /dev/null +++ b/src/modules/common/CommentActions.tsx @@ -0,0 +1,114 @@ +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Delete, Edit, Flag } from '@mui/icons-material'; +import { ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material'; + +import { APP_ACTIONS_TYPES } from '@/config/appActionsTypes'; +import { MUTATION_KEYS, useMutation } from '@/config/queryClient'; + +import { useAppDataContext } from '../context/AppDataContext'; +import { useCommentContext } from '../context/CommentContext'; +import { useReviewContext } from '../context/ReviewContext'; + +type Props = { + open: boolean; + menuAnchorEl: null | HTMLElement; + onClose: () => void; + onClickFlag?: () => void; + showDelete?: boolean; + showEdit?: boolean; + showFlag?: boolean; +}; + +const CommentActions: FC = ({ + open, + menuAnchorEl, + onClose, + onClickFlag, + showDelete = true, + showEdit = true, + showFlag = true, +}) => { + const { t } = useTranslation(); + const comment = useCommentContext(); + const { editComment } = useReviewContext(); + const { deleteAppData } = useAppDataContext(); + const { mutate: postAction } = useMutation< + unknown, + unknown, + { data: unknown; type: string } + >(MUTATION_KEYS.POST_APP_ACTION); + + return ( + onClose()} + > + {showEdit && ( + { + editComment(comment.id); + postAction({ + data: { comment }, + type: APP_ACTIONS_TYPES.EDIT_COMMENT, + }); + onClose(); + }} + > + + + + {t('Edit')} + + )} + {showDelete && ( + { + deleteAppData({ id: comment.id }); + postAction({ + data: { comment }, + type: APP_ACTIONS_TYPES.DELETE_COMMENT, + }); + onClose(); + }} + > + + + + {t('Delete')} + + )} + {showFlag && ( + { + onClickFlag?.(); + postAction({ + data: { comment }, + type: APP_ACTIONS_TYPES.REPORT_COMMENT, + }); + onClose(); + }} + > + + + + {t('Report')} + + )} + + ); +}; + +export default CommentActions; diff --git a/src/modules/common/CommentBody.tsx b/src/modules/common/CommentBody.tsx new file mode 100644 index 0000000..723d6f8 --- /dev/null +++ b/src/modules/common/CommentBody.tsx @@ -0,0 +1,133 @@ +import React, { FC, PropsWithChildren, ReactElement } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { CodeProps } from 'react-markdown/lib/ast-to-react'; + +import { styled } from '@mui/material'; + +import { Highlight, themes } from 'prism-react-renderer'; +import remarkBreaks from 'remark-breaks'; +import remarkGfm from 'remark-gfm'; + +import { BIG_BORDER_RADIUS } from '@/config/layout'; + +const StyledReactMarkdown = styled(ReactMarkdown)(({ theme }) => ({ + '& .prism-code': { + fontFamily: 'var(--monospace-fonts)', + backgroundColor: 'transparent !important', + fontSize: '0.8rem', + padding: theme.spacing(1), + }, + '& :last-child': { + marginBottom: 0, + }, + '& div.token-line': { + fontFamily: 'var(--monospace-fonts)', + }, + // set margins for all elements + '& p, ul ': { + marginBlockStart: theme.spacing(0), + marginBlockEnd: theme.spacing(1), + fontFamily: theme.typography.fontFamily, + fontSize: '1rem', + }, + '& p': { + lineHeight: '1.5', + }, + '& ul': { + // define offset for list + paddingInlineStart: theme.spacing(2), + }, + '& code': { + padding: theme.spacing(0.5, 1), + borderRadius: BIG_BORDER_RADIUS, + backgroundColor: 'var(--code-bg)', + wordWrap: 'break-word', + whiteSpace: 'pre-wrap', + fontSize: '0.9rem', + fontFamily: 'var(--monospace-fonts)', + }, + '& pre': { + margin: theme.spacing(1, 2), + backgroundColor: 'var(--code-bg)', + borderRadius: BIG_BORDER_RADIUS, + }, + '& blockquote': { + borderLeft: 'solid darkgray 4px', + color: 'darkgray', + marginLeft: '0', + paddingLeft: theme.spacing(2), + }, + '& table, th, td, tr': { + border: 'solid black 1px ', + }, + '& table': { + borderCollapse: 'collapse', + }, + // alternate background colors in table rows + '& tr:nth-of-type(even)': { + backgroundColor: 'lightgray', + }, +})); + +type Props = { + children: string; +}; + +const renderCode = ({ + inline, + className: classNameInit, + children: codeContent, + ...props +}: CodeProps): ReactElement => { + const match = /language-(\w+)/.exec(classNameInit || ''); + return !inline && match ? ( + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
+ {tokens.map((line, i) => ( + // eslint-disable-next-line react/jsx-key +
+ {line.map((token, key) => ( + // eslint-disable-next-line react/jsx-key + + ))} +
+ ))} +
+ )} +
+ ) : ( + + {codeContent} + + ); +}; + +const CommentBody: FC> = ({ children }) => ( + + {children} + +); + +export default CommentBody; diff --git a/src/modules/common/CommentEditor.tsx b/src/modules/common/CommentEditor.tsx new file mode 100644 index 0000000..1e70283 --- /dev/null +++ b/src/modules/common/CommentEditor.tsx @@ -0,0 +1,201 @@ +import React, { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + boldCommand, + codeCommand, + italicCommand, + linkCommand, + quoteCommand, + useTextAreaMarkdownEditor, +} from 'react-mde'; + +import { + Code, + FormatBold, + FormatItalic, + FormatQuote, + InsertLink, +} from '@mui/icons-material'; +import { + Box, + FormHelperText, + Stack, + TextareaAutosize, + styled, +} from '@mui/material'; + +import { Button } from '@graasp/ui'; + +import { SMALL_BORDER_RADIUS } from '@/config/layout'; +import { + COMMENT_EDITOR_BOLD_BUTTON_CYPRESS, + COMMENT_EDITOR_CANCEL_BUTTON_CYPRESS, + COMMENT_EDITOR_CODE_BUTTON_CYPRESS, + COMMENT_EDITOR_CYPRESS, + COMMENT_EDITOR_ITALIC_BUTTON_CYPRESS, + COMMENT_EDITOR_LINE_INFO_TEXT_CYPRESS, + COMMENT_EDITOR_LINK_BUTTON_CYPRESS, + COMMENT_EDITOR_QUOTE_BUTTON_CYPRESS, + COMMENT_EDITOR_SAVE_BUTTON_CYPRESS, + COMMENT_EDITOR_TEXTAREA_CYPRESS, + COMMENT_EDITOR_TEXTAREA_HELPER_TEXT_CY, +} from '@/config/selectors'; +import { DEFAULT_MAX_COMMENT_LENGTH_SETTING } from '@/config/settings'; +import { CommentType } from '@/interfaces/comment'; + +import ToolbarButton from '../layout/ToolbarButton'; + +const TextArea = styled(TextareaAutosize)(({ theme }) => ({ + borderRadius: SMALL_BORDER_RADIUS, + padding: theme.spacing(2), + fontSize: '1rem', + boxSizing: 'border-box', + resize: 'vertical', + border: 0, + outline: 'solid rgba(80, 80, 210, 0.5) 1px', + width: '100%', + minWidth: '0', + minHeight: `calc(1rem + 2*${theme.spacing(2)})`, + transition: 'outline 250ms ease-in-out', + '&:focus': { + outline: 'solid var(--graasp-primary) 2px !important', + }, + '&:hover': { + outline: 'solid var(--graasp-primary) 1px ', + }, +})); + +type Props = { + onCancel: () => void; + onSend: (comment: string) => void; + comment?: CommentType; + maxTextLength?: number; +}; + +const CommentEditor: FC = ({ + onCancel, + onSend, + comment, + maxTextLength = DEFAULT_MAX_COMMENT_LENGTH_SETTING, +}) => { + const { t } = useTranslation(); + const [text, setText] = useState(comment?.data.content ?? ''); + const [textTooLong, setTextTooLong] = useState(''); + const { ref, commandController } = useTextAreaMarkdownEditor({ + commandMap: { + bold: boldCommand, + italic: italicCommand, + code: codeCommand, + link: linkCommand, + quote: quoteCommand, + }, + }); + + // focus textarea on mount + useEffect(() => { + if (ref.current) { + ref.current.focus(); + } + }); + + const handleTextChange = ({ + target: { value }, + }: { + target: { value: string }; + }): void => { + if (value.length < maxTextLength) { + setText(value); + setTextTooLong(''); + } else { + setTextTooLong(t('COMMENT_TEXT_TOO_LONG', { max_length: maxTextLength })); + } + }; + + return ( + + + + { + await commandController.executeCommand('bold'); + }} + > + + + { + await commandController.executeCommand('italic'); + }} + > + + + { + await commandController.executeCommand('code'); + }} + > + + + { + await commandController.executeCommand('link'); + }} + > + + + { + await commandController.executeCommand('quote'); + }} + > + + + +