From 24f4c0879626c45b9c94c034eca9b06be829c8c0 Mon Sep 17 00:00:00 2001 From: rjmacarthy Date: Thu, 26 Jan 2023 13:13:46 +0000 Subject: [PATCH 01/35] Refactor task page routes and repetition Remove blank line Lint add blank line Link pre-commit --- .../e2e/tasks/no_tasks_available.cy.ts | 23 ++++++++++ website/public/locales/en/common.json | 5 ++- website/src/components/EmptyState.tsx | 2 +- website/src/components/TaskPage/TaskPage.tsx | 40 ++++++++++++++++++ website/src/lib/api.ts | 3 +- website/src/lib/constants.ts | 42 +++++++++++++++++++ website/src/pages/create/assistant_reply.tsx | 29 ++----------- website/src/pages/create/initial_prompt.tsx | 29 ++----------- website/src/pages/create/user_reply.tsx | 33 +++------------ .../pages/evaluate/rank_assistant_replies.tsx | 29 ++----------- .../pages/evaluate/rank_initial_prompts.tsx | 29 ++----------- .../src/pages/evaluate/rank_user_replies.tsx | 33 +++------------ .../src/pages/label/label_assistant_reply.tsx | 29 ++----------- .../src/pages/label/label_initial_prompt.tsx | 29 ++----------- .../src/pages/label/label_prompter_reply.tsx | 29 ++----------- website/src/pages/tasks/random.tsx | 32 ++------------ website/src/types/Task.ts | 2 +- 17 files changed, 147 insertions(+), 271 deletions(-) create mode 100644 website/cypress/e2e/tasks/no_tasks_available.cy.ts create mode 100644 website/src/components/TaskPage/TaskPage.tsx create mode 100644 website/src/lib/constants.ts diff --git a/website/cypress/e2e/tasks/no_tasks_available.cy.ts b/website/cypress/e2e/tasks/no_tasks_available.cy.ts new file mode 100644 index 00000000..27c33b48 --- /dev/null +++ b/website/cypress/e2e/tasks/no_tasks_available.cy.ts @@ -0,0 +1,23 @@ +describe("no tasks available", () => { + it("displays an empty state when no tasks are available", () => { + cy.signInWithEmail("cypress@example.com"); + cy.intercept( + { + method: "GET", + url: "/api/new_task/prompter_reply", + }, + { + statusCode: 500, + body: { + message: "No tasks of type 'label_prompter_reply' are currently available.", + errorCode: 1006, + httpStatusCode: 503, + }, + } + ).as("newTaskPrompterReply"); + cy.visit("/create/user_reply"); + cy.wait("@newTaskPrompterReply").then(() => { + cy.get('[data-cy="cy-no-tasks"]').should("exist"); + }); + }); +}); diff --git a/website/public/locales/en/common.json b/website/public/locales/en/common.json index 8f35eaab..0b0f9d37 100644 --- a/website/public/locales/en/common.json +++ b/website/public/locales/en/common.json @@ -9,11 +9,12 @@ "docs": "Docs", "github": "GitHub", "legal": "Legal", + "loading": "Loading...", + "more_information": "More Information", "privacy_policy": "Privacy Policy", "report_a_bug": "Report a Bug", "sign_in": "Sign In", "sign_out": "Sign Out", "terms_of_service": "Terms of Service", - "title": "Open Assistant", - "more_information": "More Information" + "title": "Open Assistant" } diff --git a/website/src/components/EmptyState.tsx b/website/src/components/EmptyState.tsx index b0455774..63d8d3bf 100644 --- a/website/src/components/EmptyState.tsx +++ b/website/src/components/EmptyState.tsx @@ -15,7 +15,7 @@ export const EmptyState = (props: EmptyStateProps) => { - {props.text} + {props.text} Go back to the dashboard diff --git a/website/src/components/TaskPage/TaskPage.tsx b/website/src/components/TaskPage/TaskPage.tsx new file mode 100644 index 00000000..9fc26c42 --- /dev/null +++ b/website/src/components/TaskPage/TaskPage.tsx @@ -0,0 +1,40 @@ +import Head from "next/head"; +import { useTranslation } from "next-i18next"; +import { TaskEmptyState } from "src/components/EmptyState"; +import { LoadingScreen } from "src/components/Loading/LoadingScreen"; +import { Task } from "src/components/Tasks/Task"; +import { TaskInfos } from "src/components/Tasks/TaskTypes"; +import { apiHooksByType, ERROR_CODES } from "src/lib/constants"; +import { getTypeSafei18nKey } from "src/lib/i18n"; +import { TaskType } from "src/types/Task"; + +type TaskPageProps = { + type: TaskType; +}; + +export const TaskPage = ({ type }: TaskPageProps) => { + const { t } = useTranslation(["tasks", "common"]); + const apiHook = apiHooksByType[type]; + const { tasks, isLoading, reset, trigger, error } = apiHook(type); + const taskType = TaskInfos.find((taskType) => taskType.type === type); + + if (isLoading) { + return ; + } + + if (tasks.length === 0 || error?.errorCode === ERROR_CODES.TASK_REQUESTED_TYPE_NOT_AVAILABLE) { + return ; + } + + const task = tasks[0]; + + return ( + <> + + {t(getTypeSafei18nKey(`${taskType.id}.label`))} + + + + + ); +}; diff --git a/website/src/lib/api.ts b/website/src/lib/api.ts index d61016d2..2649daf8 100644 --- a/website/src/lib/api.ts +++ b/website/src/lib/api.ts @@ -17,7 +17,8 @@ export const post = (url: string, { arg: data }) => api.post(url, data).then((re api.interceptors.response.use( (response) => response, (error) => { - throw new OasstError(error.message ?? error, error.error_code, error?.response?.status || -1); + const err = error?.response?.data; + throw new OasstError(err?.message ?? error, err?.errorCode, error?.response?.httpStatusCode || -1); } ); diff --git a/website/src/lib/constants.ts b/website/src/lib/constants.ts new file mode 100644 index 00000000..6269dbf9 --- /dev/null +++ b/website/src/lib/constants.ts @@ -0,0 +1,42 @@ +import { + useCreateAssistantReply, + useCreateInitialPrompt, + useCreatePrompterReply, +} from "src/hooks/tasks/useCreateReply"; +import { useGenericTaskAPI } from "src/hooks/tasks/useGenericTaskAPI"; +import { + useLabelAssistantReplyTask, + useLabelInitialPromptTask, + useLabelPrompterReplyTask, +} from "src/hooks/tasks/useLabelingTask"; +import { + useRankAssistantRepliesTask, + useRankInitialPromptsTask, + useRankPrompterRepliesTask, +} from "src/hooks/tasks/useRankReplies"; +import { TaskType } from "src/types/Task"; + +export const ERROR_CODES = { + TASK_REQUESTED_TYPE_NOT_AVAILABLE: 1006, + TASK_INVALID_REQUEST_TYPE: 1000, + TASK_ACK_FAILED: 1001, + TASK_NACK_FAILED: 1002, + TASK_INVALID_RESPONSE_TYPE: 1003, + TASK_INTERACTION_REQUEST_FAILED: 1004, + TASK_GENERATION_FAILED: 1005, + TASK_AVAILABILITY_QUERY_FAILED: 1007, + TASK_MESSAGE_TOO_LONG: 1008, +}; + +export const apiHooksByType = { + [TaskType.random]: useGenericTaskAPI, + [TaskType.assistant_reply]: useCreateAssistantReply, + [TaskType.initial_prompt]: useCreateInitialPrompt, + [TaskType.label_assistant_reply]: useLabelAssistantReplyTask, + [TaskType.label_initial_prompt]: useLabelInitialPromptTask, + [TaskType.label_prompter_reply]: useLabelPrompterReplyTask, + [TaskType.prompter_reply]: useCreatePrompterReply, + [TaskType.rank_assistant_replies]: useRankAssistantRepliesTask, + [TaskType.rank_initial_prompts]: useRankInitialPromptsTask, + [TaskType.rank_prompter_replies]: useRankPrompterRepliesTask, +}; diff --git a/website/src/pages/create/assistant_reply.tsx b/website/src/pages/create/assistant_reply.tsx index 1c83eb23..0f3095a9 100644 --- a/website/src/pages/create/assistant_reply.tsx +++ b/website/src/pages/create/assistant_reply.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useCreateAssistantReply } from "src/hooks/tasks/useCreateReply"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const AssistantReply = () => { - const { tasks, isLoading, reset, trigger } = useCreateAssistantReply(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Reply as Assistant - - - - - ); -}; +const AssistantReply = () => ; AssistantReply.getLayout = getDashboardLayout; diff --git a/website/src/pages/create/initial_prompt.tsx b/website/src/pages/create/initial_prompt.tsx index 639df68f..c73f2e5d 100644 --- a/website/src/pages/create/initial_prompt.tsx +++ b/website/src/pages/create/initial_prompt.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useCreateInitialPrompt } from "src/hooks/tasks/useCreateReply"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const InitialPrompt = () => { - const { tasks, isLoading, reset, trigger } = useCreateInitialPrompt(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Initial Prompt - - - - - ); -}; +const InitialPrompt = () => ; InitialPrompt.getLayout = getDashboardLayout; diff --git a/website/src/pages/create/user_reply.tsx b/website/src/pages/create/user_reply.tsx index 5898439c..39218476 100644 --- a/website/src/pages/create/user_reply.tsx +++ b/website/src/pages/create/user_reply.tsx @@ -1,33 +1,10 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useCreatePrompterReply } from "src/hooks/tasks/useCreateReply"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const UserReply = () => { - const { tasks, isLoading, reset, trigger } = useCreatePrompterReply(); +const PrompterReply = () => ; - if (isLoading) { - return ; - } +PrompterReply.getLayout = getDashboardLayout; - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Reply as User - - - - - ); -}; - -UserReply.getLayout = getDashboardLayout; - -export default UserReply; +export default PrompterReply; diff --git a/website/src/pages/evaluate/rank_assistant_replies.tsx b/website/src/pages/evaluate/rank_assistant_replies.tsx index da79d92f..dd4c1df9 100644 --- a/website/src/pages/evaluate/rank_assistant_replies.tsx +++ b/website/src/pages/evaluate/rank_assistant_replies.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useRankAssistantRepliesTask } from "src/hooks/tasks/useRankReplies"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const RankAssistantReplies = () => { - const { tasks, isLoading, reset, trigger } = useRankAssistantRepliesTask(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Rank Assistant Replies - - - - - ); -}; +const RankAssistantReplies = () => ; RankAssistantReplies.getLayout = getDashboardLayout; diff --git a/website/src/pages/evaluate/rank_initial_prompts.tsx b/website/src/pages/evaluate/rank_initial_prompts.tsx index f23fc0ed..1eb91289 100644 --- a/website/src/pages/evaluate/rank_initial_prompts.tsx +++ b/website/src/pages/evaluate/rank_initial_prompts.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useRankInitialPromptsTask } from "src/hooks/tasks/useRankReplies"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const RankInitialPrompts = () => { - const { tasks, isLoading, reset, trigger } = useRankInitialPromptsTask(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Rank Initial Prompts - - - - - ); -}; +const RankInitialPrompts = () => ; RankInitialPrompts.getLayout = getDashboardLayout; diff --git a/website/src/pages/evaluate/rank_user_replies.tsx b/website/src/pages/evaluate/rank_user_replies.tsx index cee82b87..a1caba59 100644 --- a/website/src/pages/evaluate/rank_user_replies.tsx +++ b/website/src/pages/evaluate/rank_user_replies.tsx @@ -1,33 +1,10 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useRankPrompterRepliesTask } from "src/hooks/tasks/useRankReplies"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const RankUserReplies = () => { - const { tasks, isLoading, reset, trigger } = useRankPrompterRepliesTask(); +const RankPrompterReplies = () => ; - if (isLoading) { - return ; - } +RankPrompterReplies.getLayout = getDashboardLayout; - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Rank User Replies - - - - - ); -}; - -RankUserReplies.getLayout = getDashboardLayout; - -export default RankUserReplies; +export default RankPrompterReplies; diff --git a/website/src/pages/label/label_assistant_reply.tsx b/website/src/pages/label/label_assistant_reply.tsx index 07a6cb1c..8be12b41 100644 --- a/website/src/pages/label/label_assistant_reply.tsx +++ b/website/src/pages/label/label_assistant_reply.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useLabelAssistantReplyTask } from "src/hooks/tasks/useLabelingTask"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const LabelAssistantReply = () => { - const { tasks, isLoading, trigger, reset } = useLabelAssistantReplyTask(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Label Assistant Reply - - - - - ); -}; +const LabelAssistantReply = () => ; LabelAssistantReply.getLayout = getDashboardLayout; diff --git a/website/src/pages/label/label_initial_prompt.tsx b/website/src/pages/label/label_initial_prompt.tsx index 8735044f..c5fed344 100644 --- a/website/src/pages/label/label_initial_prompt.tsx +++ b/website/src/pages/label/label_initial_prompt.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useLabelInitialPromptTask } from "src/hooks/tasks/useLabelingTask"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const LabelInitialPrompt = () => { - const { tasks, isLoading, trigger, reset } = useLabelInitialPromptTask(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Label Initial Prompt - - - - - ); -}; +const LabelInitialPrompt = () => ; LabelInitialPrompt.getLayout = getDashboardLayout; diff --git a/website/src/pages/label/label_prompter_reply.tsx b/website/src/pages/label/label_prompter_reply.tsx index 17164e11..33e8aba4 100644 --- a/website/src/pages/label/label_prompter_reply.tsx +++ b/website/src/pages/label/label_prompter_reply.tsx @@ -1,32 +1,9 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useLabelPrompterReplyTask } from "src/hooks/tasks/useLabelingTask"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; +import { TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; -const LabelPrompterReply = () => { - const { tasks, isLoading, trigger, reset } = useLabelPrompterReplyTask(); - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Label Prompter Reply - - - - - ); -}; +const LabelPrompterReply = () => ; LabelPrompterReply.getLayout = getDashboardLayout; diff --git a/website/src/pages/tasks/random.tsx b/website/src/pages/tasks/random.tsx index f1c04d2c..cd7ed458 100644 --- a/website/src/pages/tasks/random.tsx +++ b/website/src/pages/tasks/random.tsx @@ -1,34 +1,10 @@ -import Head from "next/head"; -import { TaskEmptyState } from "src/components/EmptyState"; import { getDashboardLayout } from "src/components/Layout"; -import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Task } from "src/components/Tasks/Task"; -import { useGenericTaskAPI } from "src/hooks/tasks/useGenericTaskAPI"; +import { TaskPage } from "src/components/TaskPage/TaskPage"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; import { TaskType } from "src/types/Task"; -const RandomTask = () => { - const { tasks, isLoading, trigger, reset } = useGenericTaskAPI(TaskType.random); +const Random = () => ; - if (isLoading) { - return ; - } +Random.getLayout = getDashboardLayout; - if (tasks.length === 0) { - return ; - } - - return ( - <> - - Random Task - - - - - ); -}; - -RandomTask.getLayout = (page) => getDashboardLayout(page); - -export default RandomTask; +export default Random; diff --git a/website/src/types/Task.ts b/website/src/types/Task.ts index 12e37db0..7ae48138 100644 --- a/website/src/types/Task.ts +++ b/website/src/types/Task.ts @@ -1,4 +1,4 @@ -export const enum TaskType { +export enum TaskType { initial_prompt = "initial_prompt", assistant_reply = "assistant_reply", prompter_reply = "prompter_reply", From e6009933db67de3d40cdbac87104858471dd7023 Mon Sep 17 00:00:00 2001 From: rjmacarthy Date: Fri, 27 Jan 2023 10:16:25 +0000 Subject: [PATCH 02/35] Rename const to taskInfo --- website/src/components/TaskPage/TaskPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/components/TaskPage/TaskPage.tsx b/website/src/components/TaskPage/TaskPage.tsx index 9fc26c42..41d89405 100644 --- a/website/src/components/TaskPage/TaskPage.tsx +++ b/website/src/components/TaskPage/TaskPage.tsx @@ -16,7 +16,7 @@ export const TaskPage = ({ type }: TaskPageProps) => { const { t } = useTranslation(["tasks", "common"]); const apiHook = apiHooksByType[type]; const { tasks, isLoading, reset, trigger, error } = apiHook(type); - const taskType = TaskInfos.find((taskType) => taskType.type === type); + const taskInfo = TaskInfos.find((taskType) => taskType.type === type); if (isLoading) { return ; @@ -31,8 +31,8 @@ export const TaskPage = ({ type }: TaskPageProps) => { return ( <> - {t(getTypeSafei18nKey(`${taskType.id}.label`))} - + {t(getTypeSafei18nKey(`${taskInfo.id}.label`))} + From 3b04080d7be9f07362a015b5e6b27ff463705bf7 Mon Sep 17 00:00:00 2001 From: James Melvin Ebenezer Date: Fri, 27 Jan 2023 22:36:25 +0530 Subject: [PATCH 03/35] 949_transaction error handling (#950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: transaction error handling * refactor: retry handling for all decorators as per review comments * fix: raising retry exhausted error * fix: avoid auto refresh on RollBack and review comments * removed refresh_result param from managed_tx_function --------- Co-authored-by: James Melvin Co-authored-by: Andreas Köpf --- backend/oasst_backend/utils/database_utils.py | 168 ++++++++++++------ 1 file changed, 114 insertions(+), 54 deletions(-) diff --git a/backend/oasst_backend/utils/database_utils.py b/backend/oasst_backend/utils/database_utils.py index fb8bf6c5..5f7a3136 100644 --- a/backend/oasst_backend/utils/database_utils.py +++ b/backend/oasst_backend/utils/database_utils.py @@ -7,9 +7,14 @@ from loguru import logger from oasst_backend.config import settings from oasst_backend.database import engine from oasst_shared.exceptions import OasstError, OasstErrorCode -from sqlalchemy.exc import OperationalError +from psycopg2.errors import DeadlockDetected, ExclusionViolation, SerializationFailure, UniqueViolation +from sqlalchemy.exc import OperationalError, PendingRollbackError from sqlmodel import Session, SQLModel +""" +Error Handling Reference: https://www.postgresql.org/docs/15/mvcc-serialization-failure-handling.html +""" + class CommitMode(IntEnum): """ @@ -34,28 +39,46 @@ def managed_tx_method(auto_commit: CommitMode = CommitMode.COMMIT, num_retries=s @wraps(f) def wrapped_f(self, *args, **kwargs): try: - for i in range(num_retries): - try: - result = f(self, *args, **kwargs) - if auto_commit == CommitMode.COMMIT: + result = None + if auto_commit == CommitMode.COMMIT: + retry_exhausted = True + for i in range(num_retries): + try: + result = f(self, *args, **kwargs) self.db.commit() - elif auto_commit == CommitMode.FLUSH: - self.db.flush() - elif auto_commit == CommitMode.ROLLBACK: + if isinstance(result, SQLModel): + self.db.refresh(result) + retry_exhausted = False + break + except PendingRollbackError as e: + logger.info(str(e)) self.db.rollback() + except OperationalError as e: + if e.orig is not None and isinstance( + e.orig, (SerializationFailure, DeadlockDetected, UniqueViolation, ExclusionViolation) + ): + logger.info(f"{type(e.orig)} Inner {e.orig.pgcode} {type(e.orig.pgcode)}") + self.db.rollback() + else: + raise e + logger.info(f"Retry {i+1}/{num_retries}") + if retry_exhausted: + raise OasstError( + "DATABASE_MAX_RETIRES_EXHAUSTED", + error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, + http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + else: + result = f(self, *args, **kwargs) + if auto_commit == CommitMode.FLUSH: + self.db.flush() if isinstance(result, SQLModel): self.db.refresh(result) - return result - except OperationalError: - logger.info(f"Retry {i+1}/{num_retries} after possible DB concurrent update conflict.") + elif auto_commit == CommitMode.ROLLBACK: self.db.rollback() - raise OasstError( - "DATABASE_MAX_RETIRES_EXHAUSTED", - error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, - http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, - ) + return result except Exception as e: - logger.error("DB Rollback Failure") + logger.info(str(e)) raise e return wrapped_f @@ -70,28 +93,46 @@ def async_managed_tx_method( @wraps(f) async def wrapped_f(self, *args, **kwargs): try: - for i in range(num_retries): - try: - result = await f(self, *args, **kwargs) - if auto_commit == CommitMode.COMMIT: + result = None + if auto_commit == CommitMode.COMMIT: + retry_exhausted = True + for i in range(num_retries): + try: + result = f(self, *args, **kwargs) self.db.commit() - elif auto_commit == CommitMode.FLUSH: - self.db.flush() - elif auto_commit == CommitMode.ROLLBACK: + if isinstance(result, SQLModel): + self.db.refresh(result) + retry_exhausted = False + break + except PendingRollbackError as e: + logger.info(str(e)) self.db.rollback() + except OperationalError as e: + if e.orig is not None and isinstance( + e.orig, (SerializationFailure, DeadlockDetected, UniqueViolation, ExclusionViolation) + ): + logger.info(f"{type(e.orig)} Inner {e.orig.pgcode} {type(e.orig.pgcode)}") + self.db.rollback() + else: + raise e + logger.info(f"Retry {i+1}/{num_retries}") + if retry_exhausted: + raise OasstError( + "DATABASE_MAX_RETIRES_EXHAUSTED", + error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, + http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + else: + result = f(self, *args, **kwargs) + if auto_commit == CommitMode.FLUSH: + self.db.flush() if isinstance(result, SQLModel): self.db.refresh(result) - return result - except OperationalError: - logger.info(f"Retry {i+1}/{num_retries} after possible DB concurrent update conflict.") + elif auto_commit == CommitMode.ROLLBACK: self.db.rollback() - raise OasstError( - "DATABASE_MAX_RETIRES_EXHAUSTED", - error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, - http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, - ) + return result except Exception as e: - logger.exception("DB Rollback Failure") + logger.info(str(e)) raise e return wrapped_f @@ -107,7 +148,6 @@ def managed_tx_function( auto_commit: CommitMode = CommitMode.COMMIT, num_retries=settings.DATABASE_MAX_TX_RETRY_COUNT, session_factory: Callable[..., Session] = default_session_factor, - refresh_result: bool = True, ): """Passes Session object as first argument to wrapped function.""" @@ -115,29 +155,49 @@ def managed_tx_function( @wraps(f) def wrapped_f(*args, **kwargs): try: - for i in range(num_retries): - with session_factory() as session: - try: - result = f(session, *args, **kwargs) - if auto_commit == CommitMode.COMMIT: + result = None + if auto_commit == CommitMode.COMMIT: + retry_exhausted = True + for i in range(num_retries): + with session_factory() as session: + try: + result = f(session, *args, **kwargs) session.commit() - elif auto_commit == CommitMode.FLUSH: - session.flush() - elif auto_commit == CommitMode.ROLLBACK: + if isinstance(result, SQLModel): + session.refresh(result) + retry_exhausted = False + break + except PendingRollbackError as e: + logger.info(str(e)) session.rollback() - if refresh_result and isinstance(result, SQLModel): - session.refresh(result) - return result - except OperationalError: - logger.info(f"Retry {i+1}/{num_retries} after possible DB concurrent update conflict.") - session.rollback() - raise OasstError( - "DATABASE_MAX_RETIRES_EXHAUSTED", - error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, - http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, - ) + except OperationalError as e: + if e.orig is not None and isinstance( + e.orig, + (SerializationFailure, DeadlockDetected, UniqueViolation, ExclusionViolation), + ): + logger.info(f"{type(e.orig)} Inner {e.orig.pgcode} {type(e.orig.pgcode)}") + session.rollback() + else: + raise e + logger.info(f"Retry {i+1}/{num_retries}") + if retry_exhausted: + raise OasstError( + "DATABASE_MAX_RETIRES_EXHAUSTED", + error_code=OasstErrorCode.DATABASE_MAX_RETRIES_EXHAUSTED, + http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + else: + with session_factory() as session: + result = f(session, *args, **kwargs) + if auto_commit == CommitMode.FLUSH: + session.flush() + if isinstance(result, SQLModel): + session.refresh(result) + elif auto_commit == CommitMode.ROLLBACK: + session.rollback() + return result except Exception as e: - logger.error("DB Rollback Failure") + logger.info(str(e)) raise e return wrapped_f From ce3b3c7eccd8aadf288b6c67c685c5ec805a97e9 Mon Sep 17 00:00:00 2001 From: notmd Date: Sat, 28 Jan 2023 00:17:31 +0700 Subject: [PATCH 04/35] pass correct param when fetch leaderboard --- website/src/lib/oasst_api_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index bd263400..3cc4f4ef 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -144,7 +144,7 @@ export class OasstApiClient { time_frame: LeaderboardTimeFrame, { limit = 20 }: { limit?: number } ): Promise { - return this.get(`/api/v1/leaderboards/${time_frame}`, { limit }); + return this.get(`/api/v1/leaderboards/${time_frame}`, { max_count: limit }); } /** From d16598725664241c554c466ebf30a9b2900af4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Fri, 27 Jan 2023 19:43:08 +0100 Subject: [PATCH 05/35] add missing await to async_managed_tx_method --- backend/oasst_backend/utils/database_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/oasst_backend/utils/database_utils.py b/backend/oasst_backend/utils/database_utils.py index 5f7a3136..196178e5 100644 --- a/backend/oasst_backend/utils/database_utils.py +++ b/backend/oasst_backend/utils/database_utils.py @@ -98,7 +98,7 @@ def async_managed_tx_method( retry_exhausted = True for i in range(num_retries): try: - result = f(self, *args, **kwargs) + result = await f(self, *args, **kwargs) self.db.commit() if isinstance(result, SQLModel): self.db.refresh(result) From c7692b9049a25ff00c6e39d2854a125cbbc5411d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Fri, 27 Jan 2023 19:44:48 +0100 Subject: [PATCH 06/35] add 2nd missing await to async_managed_tx_method --- backend/oasst_backend/utils/database_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/oasst_backend/utils/database_utils.py b/backend/oasst_backend/utils/database_utils.py index 196178e5..5ac25f50 100644 --- a/backend/oasst_backend/utils/database_utils.py +++ b/backend/oasst_backend/utils/database_utils.py @@ -123,7 +123,7 @@ def async_managed_tx_method( http_status_code=HTTPStatus.SERVICE_UNAVAILABLE, ) else: - result = f(self, *args, **kwargs) + result = await f(self, *args, **kwargs) if auto_commit == CommitMode.FLUSH: self.db.flush() if isinstance(result, SQLModel): From bf77d4dc60d1a295c02242feec3e8c9ce1e08835 Mon Sep 17 00:00:00 2001 From: rjmacarthy Date: Fri, 27 Jan 2023 10:51:32 +0000 Subject: [PATCH 07/35] Add types for TaskType to TaskHook Pre-commit Apply better naming to task api hooks Lint --- website/src/components/TaskPage/TaskPage.tsx | 6 ++--- website/src/lib/constants.ts | 3 ++- website/src/types/Hooks.ts | 26 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 website/src/types/Hooks.ts diff --git a/website/src/components/TaskPage/TaskPage.tsx b/website/src/components/TaskPage/TaskPage.tsx index 41d89405..e1ecc79c 100644 --- a/website/src/components/TaskPage/TaskPage.tsx +++ b/website/src/components/TaskPage/TaskPage.tsx @@ -4,7 +4,7 @@ import { TaskEmptyState } from "src/components/EmptyState"; import { LoadingScreen } from "src/components/Loading/LoadingScreen"; import { Task } from "src/components/Tasks/Task"; import { TaskInfos } from "src/components/Tasks/TaskTypes"; -import { apiHooksByType, ERROR_CODES } from "src/lib/constants"; +import { ERROR_CODES, taskApiHooks } from "src/lib/constants"; import { getTypeSafei18nKey } from "src/lib/i18n"; import { TaskType } from "src/types/Task"; @@ -14,8 +14,8 @@ type TaskPageProps = { export const TaskPage = ({ type }: TaskPageProps) => { const { t } = useTranslation(["tasks", "common"]); - const apiHook = apiHooksByType[type]; - const { tasks, isLoading, reset, trigger, error } = apiHook(type); + const taskApiHook = taskApiHooks[type]; + const { tasks, isLoading, reset, trigger, error } = taskApiHook(type); const taskInfo = TaskInfos.find((taskType) => taskType.type === type); if (isLoading) { diff --git a/website/src/lib/constants.ts b/website/src/lib/constants.ts index 6269dbf9..a260fa28 100644 --- a/website/src/lib/constants.ts +++ b/website/src/lib/constants.ts @@ -14,6 +14,7 @@ import { useRankInitialPromptsTask, useRankPrompterRepliesTask, } from "src/hooks/tasks/useRankReplies"; +import { TaskApiHooks } from "src/types/Hooks"; import { TaskType } from "src/types/Task"; export const ERROR_CODES = { @@ -28,7 +29,7 @@ export const ERROR_CODES = { TASK_MESSAGE_TOO_LONG: 1008, }; -export const apiHooksByType = { +export const taskApiHooks: TaskApiHooks = { [TaskType.random]: useGenericTaskAPI, [TaskType.assistant_reply]: useCreateAssistantReply, [TaskType.initial_prompt]: useCreateInitialPrompt, diff --git a/website/src/types/Hooks.ts b/website/src/types/Hooks.ts new file mode 100644 index 00000000..8fd9aa4f --- /dev/null +++ b/website/src/types/Hooks.ts @@ -0,0 +1,26 @@ +import { MutatorCallback, MutatorOptions } from "swr"; + +import { BaseTask, TaskResponse, TaskType } from "./Task"; + +type ConcreteTaskResponse = TaskResponse; +type TaskError = { errorCode: number; message: string }; + +type Trigger = ( + extraArgument?: unknown, + options?: MutatorOptions +) => Promise; + +type Reset = ( + data?: ConcreteTaskResponse | Promise | MutatorCallback, + opts?: boolean | MutatorOptions +) => Promise; + +type TaskAPIHook = { + tasks: TaskResponse[]; + isLoading: boolean; + error: TaskError; + trigger: Trigger; + reset: Reset; +}; + +export type TaskApiHooks = Record TaskAPIHook>; From 356fd775e93505b32535ca628fa142f987ab0415 Mon Sep 17 00:00:00 2001 From: Adrian Cowan Date: Sat, 28 Jan 2023 06:52:40 +1100 Subject: [PATCH 08/35] Add emoji reactions and reporting for messages (website) (#952) * website: Move labelling to message ... menu and add reporting and emoji reactions We can add more emoji easily in future, we just need to pick ones that we have consistent icons for. Also added "open in new tab" option so that messages can be navigated to from tasks on mobile. * website: Make new label and report strings translatable. * website: Move report api call to oasst client * small fixes * pre-commit --------- Co-authored-by: AbdBarho --- website/public/locales/en/message.json | 11 ++ website/src/components/Messages.tsx | 2 +- .../src/components/Messages/LabelPopup.tsx | 76 ++++++++ .../Messages/MessageEmojiButton.stories.tsx | 34 ++++ .../Messages/MessageEmojiButton.tsx | 48 +++++ .../Messages/MessageTable.stories.tsx | 23 ++- .../src/components/Messages/MessageTable.tsx | 6 +- .../Messages/MessageTableEntry.stories.tsx | 25 ++- .../components/Messages/MessageTableEntry.tsx | 176 +++++++++++++++--- .../Messages/MessageWithChildren.tsx | 6 +- .../src/components/Messages/ReportPopup.tsx | 56 ++++++ website/src/components/Tasks/EvaluateTask.tsx | 1 - .../components/Tasks/LabelTask/LabelTask.tsx | 2 +- website/src/lib/oasst_api_client.ts | 34 +++- website/src/pages/api/messages/[id]/emoji.ts | 30 +++ website/src/pages/api/messages/[id]/index.ts | 11 +- website/src/pages/api/report.ts | 24 +++ website/src/pages/api/set_label.ts | 6 +- website/src/pages/messages/[id]/index.tsx | 2 +- website/src/types/Conversation.ts | 16 +- website/styles/Theme/colors.tsx | 2 + website/types/i18next.d.ts | 4 +- 22 files changed, 541 insertions(+), 54 deletions(-) create mode 100644 website/public/locales/en/message.json create mode 100644 website/src/components/Messages/LabelPopup.tsx create mode 100644 website/src/components/Messages/MessageEmojiButton.stories.tsx create mode 100644 website/src/components/Messages/MessageEmojiButton.tsx create mode 100644 website/src/components/Messages/ReportPopup.tsx create mode 100644 website/src/pages/api/messages/[id]/emoji.ts create mode 100644 website/src/pages/api/report.ts diff --git a/website/public/locales/en/message.json b/website/public/locales/en/message.json new file mode 100644 index 00000000..45ea04a1 --- /dev/null +++ b/website/public/locales/en/message.json @@ -0,0 +1,11 @@ +{ + "reactions": "Reactions", + "label_action": "Label", + "label_title": "Label", + "submit_labels": "Submit", + "open_new_tab_action": "Open in new tab", + "report_title": "Report", + "report_action": "Report", + "report_placeholder": "Why should this message be reviewed?", + "send_report": "Send" +} diff --git a/website/src/components/Messages.tsx b/website/src/components/Messages.tsx index 0934c5af..c9d77e3c 100644 --- a/website/src/components/Messages.tsx +++ b/website/src/components/Messages.tsx @@ -20,7 +20,7 @@ export const Messages = ({ messages }: MessagesProps) => { return {items}; }; -export const MessageView = forwardRef((message: Message, ref) => { +export const MessageView = forwardRef, "div">((message: Partial, ref) => { const { colorMode } = useColorMode(); const bgColor = useMemo(() => { diff --git a/website/src/components/Messages/LabelPopup.tsx b/website/src/components/Messages/LabelPopup.tsx new file mode 100644 index 00000000..b2b95278 --- /dev/null +++ b/website/src/components/Messages/LabelPopup.tsx @@ -0,0 +1,76 @@ +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from "@chakra-ui/react"; +import { useTranslation } from "next-i18next"; +import { useState } from "react"; +import { LabelInputGroup } from "src/components/Survey/LabelInputGroup"; +import { get, post } from "src/lib/api"; +import useSWRImmutable from "swr/immutable"; +import useSWRMutation from "swr/mutation"; + +interface LabelMessagePopupProps { + messageId: string; + show: boolean; + onClose: () => void; +} + +interface Label { + name: string; + display_text: string; + help_text: string; +} + +interface ValidLabelsResponse { + valid_labels: Label[]; +} + +export const LabelMessagePopup = ({ messageId, show, onClose }: LabelMessagePopupProps) => { + const { t } = useTranslation("message"); + const { data: response } = useSWRImmutable("/api/valid_labels", get); + const valid_labels = response?.valid_labels ?? []; + const [values, setValues] = useState(null); + + const { trigger: setLabels } = useSWRMutation("/api/set_label", post); + + const submit = () => { + const label_map: Map = new Map(); + console.assert(valid_labels.length === values.length); + values.forEach((value, idx) => { + if (value !== null) { + label_map.set(valid_labels[idx].name, value); + } + }); + setLabels({ + message_id: messageId, + label_map: Object.fromEntries(label_map), + }); + + setValues(null); + onClose(); + }; + + return ( + + + + {t("label_title")} + + + name)} onChange={setValues} /> + + + + + + + ); +}; diff --git a/website/src/components/Messages/MessageEmojiButton.stories.tsx b/website/src/components/Messages/MessageEmojiButton.stories.tsx new file mode 100644 index 00000000..d74836d5 --- /dev/null +++ b/website/src/components/Messages/MessageEmojiButton.stories.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import { MessageEmojiButton } from "./MessageEmojiButton"; + +// eslint-disable-next-line import/no-anonymous-default-export +export default { + title: "Messages/MessageEmojiButton", + component: MessageEmojiButton, +}; + +const Template = ({ emoji, count, checked }: { emoji: string; count: number; checked?: boolean }) => { + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + emoji: "+1", + count: 7, + checked: false, +}; + +export const BigNumber = Template.bind({}); +BigNumber.args = { + emoji: "+1", + count: 999, + checked: false, +}; + +export const Checked = Template.bind({}); +Checked.args = { + emoji: "+1", + count: 2, + checked: true, +}; diff --git a/website/src/components/Messages/MessageEmojiButton.tsx b/website/src/components/Messages/MessageEmojiButton.tsx new file mode 100644 index 00000000..8b5c9ff7 --- /dev/null +++ b/website/src/components/Messages/MessageEmojiButton.tsx @@ -0,0 +1,48 @@ +import { Button } from "@chakra-ui/react"; +import { BoxSelect, Flag, LucideProps, ThumbsDown, ThumbsUp } from "lucide-react"; +import { ReactElement } from "react"; +import { MessageEmoji } from "src/types/Conversation"; + +type EmojiIconPurpose = "MINI_BUTTON" | "NORMAL"; + +const defaultIconProps: (purpose: EmojiIconPurpose) => LucideProps = (purpose: EmojiIconPurpose) => { + if (purpose === "MINI_BUTTON") return { height: "1em" }; + return {}; +}; + +export const getEmojiIcon = (name: string, purpose: EmojiIconPurpose): ReactElement => { + switch (name) { + case "+1": + return ; + case "-1": + return ; + case "flag": + case "red_flag": + return ; + default: + return ; + } +}; + +interface MessageEmojiButtonProps { + emoji: MessageEmoji; + checked?: boolean; + onClick: () => void; +} + +export const MessageEmojiButton = ({ emoji, checked, onClick }: MessageEmojiButtonProps) => { + return ( + + ); +}; diff --git a/website/src/components/Messages/MessageTable.stories.tsx b/website/src/components/Messages/MessageTable.stories.tsx index 6f383b01..bc03aed1 100644 --- a/website/src/components/Messages/MessageTable.stories.tsx +++ b/website/src/components/Messages/MessageTable.stories.tsx @@ -29,18 +29,24 @@ Default.args = { is_assistant: true, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, { text: "No, I just wanted to see how you reply when I type random characters. Can you tell me who invented Wikipedia?", is_assistant: false, id: "", frontend_message_id: "", + emojis: { "-1": 11, red_flag: 2 }, + user_emojis: [], }, { text: "Sorry, my cat sat on my keyboard. Can you print a cat in ASCII art?", is_assistant: false, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, ], enableLink: true, @@ -50,12 +56,21 @@ Default.args = { export const Conversation = Template.bind({}); Conversation.args = { messages: [ - { text: "Hello! How can I help you?", is_assistant: true, id: "", frontend_message_id: "" }, + { + text: "Hello! How can I help you?", + is_assistant: true, + id: "", + frontend_message_id: "", + emojis: {}, + user_emojis: [], + }, { text: "Who were the 8 presidents before George Washington?", is_assistant: false, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, ], enableLink: false, @@ -70,18 +85,24 @@ LongText.args = { is_assistant: true, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, { text: "Yes, I think they can be helpful when the child misbehaves, but they should be used with a little bit of compassion and understanding that it\u2019s not the natural state of things to have an adult yelling at them. Time outs are also often used without letting the child know how they\u2019re getting out of the time out, which can make it feel arbitrary or like a punishment, rather than a consequence for something they did. It\u2019s really easy for adults to do this kind of thing unconsciously. It\u2019s easy to get caught up in the notion that \u201cThey\u2019re in time out, and that\u2019s the end of it!\u201d but kids can be pretty imaginative, and they can use their own creativity to make their way out of time outs. A compassionate time out ends when the child shows a sign of understanding what they\u2019ve done wrong, and are ready to begin again. That way the child knows they\u2019re learning, and that the parent is seeing them as an intelligent person, even if they sometimes mess up. You can still use the other techniques you were using to be tough when necessary, but using a compassionate approach will let you use them without actually using them!", is_assistant: false, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, { text: "No. The USA was founded by a Puritan group of Protestants, but it didn\u2019t adopt the religion of the Puritans until much later, and it was always a secular state. The Puritans observed the Sabbath on Sunday, and the Puritans only had a small influence in the early history of the USA. It\u2019s difficult to trace the origins of closing stores on Sunday, but one early and short-lived attempt at forcing the Sabbath on people in the 1800s was motivated by the Protestant ideal that people should spend Sunday focusing on spiritual activities. By the mid-1800s, when the Sunday closing law was made, there was not a lot of pressure from that standpoint, but the church had begun to advocate for Sunday closing laws as a way of counteracting the negative effects of industrialization on the day of rest. Even after that shift, closing stores on Sunday was not always possible, since the religious Sunday was not always chosen for observance. And as industrialization accelerated and mechanization made it possible to operate stores on Sunday, the law was not enforced as much as people liked. The day of rest was also being violated by stores that stayed open all day on Sunday, so closing stores on Sundays became an effort to protect the Sabbath for all citizens.", is_assistant: false, id: "", frontend_message_id: "", + emojis: {}, + user_emojis: [], }, ], enableLink: true, diff --git a/website/src/components/Messages/MessageTable.tsx b/website/src/components/Messages/MessageTable.tsx index acf92e05..2d39f346 100644 --- a/website/src/components/Messages/MessageTable.tsx +++ b/website/src/components/Messages/MessageTable.tsx @@ -11,11 +11,11 @@ interface MessageTableProps { export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) { return ( - {messages.map((item, idx) => ( + {messages.map((message, idx) => ( ))} diff --git a/website/src/components/Messages/MessageTableEntry.stories.tsx b/website/src/components/Messages/MessageTableEntry.stories.tsx index b6071dd7..3550d00f 100644 --- a/website/src/components/Messages/MessageTableEntry.stories.tsx +++ b/website/src/components/Messages/MessageTableEntry.stories.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Message } from "src/types/Conversation"; import { MessageTableEntry } from "./MessageTableEntry"; @@ -8,10 +9,8 @@ export default { component: MessageTableEntry, }; -const Template = ({ text, is_assistant, id, frontend_message_id, enabled, highlight }) => { - return ( - - ); +const Template = ({ enabled, highlight, ...message }) => { + return ; }; export const Default = Template.bind({}); @@ -22,6 +21,8 @@ Default.args = { frontend_message_id: "", enabled: true, highlight: false, + emojis: {}, + user_emojis: [], }; export const Asistant = Template.bind({}); @@ -32,6 +33,8 @@ Asistant.args = { frontend_message_id: "", enabled: true, highlight: false, + emojis: {}, + user_emojis: [], }; export const LongText = Template.bind({}); @@ -42,4 +45,18 @@ LongText.args = { frontend_message_id: "", enabled: true, highlight: false, + emojis: {}, + user_emojis: [], +}; + +export const WithEmoji = Template.bind({}); +WithEmoji.args = { + text: "As you\u2019ve mentioned, Star Wars has many sequels, prequels, and crossovers. The official list of movies in Star Wars is:", + is_assistant: true, + id: "", + frontend_message_id: "", + enabled: true, + highlight: false, + emojis: { "-1": 5, "+1": 1 }, + user_emojis: ["-1"], }; diff --git a/website/src/components/Messages/MessageTableEntry.tsx b/website/src/components/Messages/MessageTableEntry.tsx index 77202c44..3cde48f6 100644 --- a/website/src/components/Messages/MessageTableEntry.tsx +++ b/website/src/components/Messages/MessageTableEntry.tsx @@ -1,23 +1,47 @@ -import { Avatar, Box, HStack, useBreakpointValue, useColorModeValue } from "@chakra-ui/react"; +import { + Avatar, + Box, + HStack, + Menu, + MenuButton, + MenuDivider, + MenuGroup, + MenuItem, + MenuList, + SimpleGrid, + useBreakpointValue, + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; import { boolean } from "boolean"; +import { ClipboardList, Flag, MessageSquare, MoreHorizontal } from "lucide-react"; import { useRouter } from "next/router"; -import { useCallback, useMemo } from "react"; -import { FlaggableElement } from "src/components/FlaggableElement"; -import { Message } from "src/types/Conversation"; +import { useTranslation } from "next-i18next"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { LabelMessagePopup } from "src/components/Messages/LabelPopup"; +import { getEmojiIcon, MessageEmojiButton } from "src/components/Messages/MessageEmojiButton"; +import { ReportPopup } from "src/components/Messages/ReportPopup"; +import { post } from "src/lib/api"; +import { Message, MessageEmojis } from "src/types/Conversation"; import { colors } from "styles/Theme/colors"; +import useSWRMutation from "swr/mutation"; interface MessageTableEntryProps { - item: Message; + message: Message; enabled?: boolean; highlight?: boolean; } -export function MessageTableEntry(props: MessageTableEntryProps) { +export function MessageTableEntry({ message, enabled, highlight }: MessageTableEntryProps) { const router = useRouter(); + const [emojis, setEmojis] = useState({ emojis: {}, user_emojis: [] }); + useEffect(() => { + setEmojis({ emojis: message.emojis, user_emojis: message.user_emojis }); + }, [message.emojis, message.user_emojis]); - const { item } = props; - - const goToMessage = useCallback(() => router.push(`/messages/${item.id}`), [router, item.id]); + const goToMessage = useCallback(() => router.push(`/messages/${message.id}`), [router, message.id]); + const { isOpen: reportPopupOpen, onOpen: showReportPopup, onClose: closeReportPopup } = useDisclosure(); + const { isOpen: labelPopupOpen, onOpen: showLabelPopup, onClose: closeLabelPopup } = useDisclosure(); const backgroundColor = useColorModeValue("gray.100", "gray.700"); const backgroundColor2 = useColorModeValue("#DFE8F1", "#42536B"); @@ -32,34 +56,124 @@ export function MessageTableEntry(props: MessageTableEntryProps) { borderColor={borderColor} size={inlineAvatar ? "xs" : "sm"} mr={inlineAvatar ? 2 : 0} - name={`${boolean(item.is_assistant) ? "Assistant" : "User"}`} - src={`${boolean(item.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`} + name={`${boolean(message.is_assistant) ? "Assistant" : "User"}`} + src={`${boolean(message.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`} /> ), - [borderColor, inlineAvatar, item.is_assistant] + [borderColor, inlineAvatar, message.is_assistant] ); const highlightColor = useColorModeValue(colors.light.highlight, colors.dark.highlight); + const { trigger: sendEmojiChange } = useSWRMutation(`/api/messages/${message.id}/emoji`, post, { + onSuccess: setEmojis, + }); + const react = (emoji: string, state: boolean) => { + sendEmojiChange({ op: state ? "add" : "remove", emoji }); + }; + return ( - - - {!inlineAvatar && avatar} - + {!inlineAvatar && avatar} + + {inlineAvatar && avatar} + {message.text} + e.stopPropagation()} > - {inlineAvatar && avatar} - {item.text} - - - + {Object.entries(emojis.emojis).map(([emoji, count]) => ( + react(emoji, !emojis.user_emojis.includes(emoji))} + /> + ))} + + + + + + ); } + +const EmojiMenuItem = ({ + emoji, + checked, + react, +}: { + emoji: string; + checked?: boolean; + react: (emoji: string, state: boolean) => void; +}) => { + const activeColor = useColorModeValue(colors.light.active, colors.dark.active); + + return ( + react(emoji, !checked)} justifyContent="center" color={checked ? activeColor : undefined}> + {getEmojiIcon(emoji, "NORMAL")} + + ); +}; + +const MessageActions = ({ + react, + userEmoji, + onLabel, + onReport, + messageId, +}: { + react: (emoji: string, state: boolean) => void; + userEmoji: string[]; + onLabel: () => void; + onReport: () => void; + messageId: string; +}) => { + const { t } = useTranslation("message"); + + return ( + + + + + + + + {["+1", "-1"].map((emoji) => ( + + ))} + + + + }> + {t("label_action")} + + }> + {t("report_action")} + + + }> + {t("open_new_tab_action")} + + + + ); +}; diff --git a/website/src/components/Messages/MessageWithChildren.tsx b/website/src/components/Messages/MessageWithChildren.tsx index ca29c410..f47cfac5 100644 --- a/website/src/components/Messages/MessageWithChildren.tsx +++ b/website/src/components/Messages/MessageWithChildren.tsx @@ -52,7 +52,7 @@ export function MessageWithChildren(props: MessageWithChildrenProps) { {isFirst ? "Message" : depth === 1 ? "Children" : "Ancestor"} - + @@ -86,9 +86,9 @@ export function MessageWithChildren(props: MessageWithChildrenProps) { gap="4" shadow="base" > - {children.map((item, idx) => ( + {children.map((message, idx) => ( - + ))} diff --git a/website/src/components/Messages/ReportPopup.tsx b/website/src/components/Messages/ReportPopup.tsx new file mode 100644 index 00000000..67a2ea1b --- /dev/null +++ b/website/src/components/Messages/ReportPopup.tsx @@ -0,0 +1,56 @@ +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Textarea, +} from "@chakra-ui/react"; +import { useTranslation } from "next-i18next"; +import { useState } from "react"; +import { post } from "src/lib/api"; +import useSWRMutation from "swr/mutation"; + +interface ReportPopupProps { + messageId: string; + show: boolean; + onClose: () => void; +} + +export const ReportPopup = ({ messageId, show, onClose }: ReportPopupProps) => { + const { t } = useTranslation("message"); + const [text, setText] = useState(""); + const { trigger } = useSWRMutation("/api/report", post); + + const submit = () => { + trigger({ + message_id: messageId, + text, + }); + + setText(""); + onClose(); + }; + + return ( + + + + {t("report_title")} + + +