diff --git a/website/src/components/Dashboard/TaskOption.tsx b/website/src/components/Dashboard/TaskOption.tsx index 3497d14c..0401fd97 100644 --- a/website/src/components/Dashboard/TaskOption.tsx +++ b/website/src/components/Dashboard/TaskOption.tsx @@ -15,9 +15,9 @@ import Link from "next/link"; import { useTranslation } from "next-i18next"; import { useMemo } from "react"; import { getTypeSafei18nKey } from "src/lib/i18n"; -import { TaskType } from "src/types/Task"; +import { TaskCategory, TaskInfo, TaskType } from "src/types/Task"; -import { TaskCategory, TaskCategoryLabels, TaskInfo, TaskInfos } from "../Tasks/TaskTypes"; +import { TaskCategoryLabels, TaskInfos } from "../Tasks/TaskTypes"; export interface TasksOptionProps { content: Partial>; diff --git a/website/src/components/TaskPage/TaskPage.tsx b/website/src/components/TaskPage/TaskPage.tsx index 77b1bfe9..b6242702 100644 --- a/website/src/components/TaskPage/TaskPage.tsx +++ b/website/src/components/TaskPage/TaskPage.tsx @@ -1,13 +1,14 @@ import Head from "next/head"; import { useTranslation } from "next-i18next"; +import { useMemo } from "react"; 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 { TaskContext } from "src/context/TaskContext"; import { taskApiHooks } from "src/lib/constants"; import { getTypeSafei18nKey } from "src/lib/i18n"; import { TaskType } from "src/types/Task"; -import { KnownTaskType } from "src/types/Tasks"; type TaskPageProps = { type: TaskType; @@ -15,32 +16,32 @@ type TaskPageProps = { export const TaskPage = ({ type }: TaskPageProps) => { const { t } = useTranslation(["tasks", "common"]); - const taskApiHook = taskApiHooks[type]; - const { response, isLoading, completeTask, skipTask } = taskApiHook(type); + const taskApiHook = useMemo(() => taskApiHooks[type], [type]); + const hookState = taskApiHook(type); + + const body = useMemo(() => { + const { response } = hookState; + switch (response.taskAvailability) { + case "AWAITING_INITIAL": + return ; + + case "NONE_AVAILABLE": + return ; + + case "AVAILABLE": { + const { task, taskInfo } = response; + const context = { ...hookState, task, taskInfo }; + return ( + + + + ); + } + } + }, [hookState, t]); + + // NOTE: this is independent of the fetched task type, it is usually identical, but not for the random task. const taskInfo = TaskInfos.find((taskType) => taskType.type === type); - - let body; - switch (response.taskAvailability) { - case "AWAITING_INITIAL": - body = ; - break; - case "NONE_AVAILABLE": - body = ; - break; - case "AVAILABLE": - body = ( - - ); - break; - } - return ( <> diff --git a/website/src/components/Tasks/Task/Task.stories.tsx b/website/src/components/Tasks/Task/Task.stories.tsx index fe7122b9..cb0cd5c3 100644 --- a/website/src/components/Tasks/Task/Task.stories.tsx +++ b/website/src/components/Tasks/Task/Task.stories.tsx @@ -1,20 +1,22 @@ import React from "react"; - -import { Task } from "./Task"; +import { Task } from "src/components/Tasks/Task"; +import { TaskInfos } from "src/components/Tasks/TaskTypes"; +import { TaskContext } from "src/context/TaskContext"; export default { title: "tasks/Task", component: Task, }; -const Template = ({ frontendId, task, isLoading, completeTask, skipTask }) => { +const Template = ({ providerValue }) => { return ( - + + + ); }; -export const Default = Template.bind({}); -Default.args = { +const exampleProviderValue = { frontendId: "1234", task: { conversation: [], @@ -25,11 +27,18 @@ Default.args = { type: "label_prompter_reply", valid_labels: ["spam", "fails_task"], }, + taskInfo: TaskInfos.find((t) => t.type === "label_prompter_reply"), isLoading: false, - completeTask: (id, update_type, content) => { + completeTask: (content) => { console.log(content); }, skipTask: () => { console.log("skip"); }, + rejectTask: () => { + console.log("reject"); + }, }; + +export const Default = Template.bind({}); +Default.args = { providerValue: exampleProviderValue }; diff --git a/website/src/components/Tasks/Task/Task.tsx b/website/src/components/Tasks/Task/Task.tsx index 738c3667..f251cdd0 100644 --- a/website/src/components/Tasks/Task/Task.tsx +++ b/website/src/components/Tasks/Task/Task.tsx @@ -5,13 +5,12 @@ import { TaskControls } from "src/components/Survey/TaskControls"; import { CreateTask } from "src/components/Tasks/CreateTask"; import { EvaluateTask } from "src/components/Tasks/EvaluateTask"; import { LabelTask } from "src/components/Tasks/LabelTask"; -import { TaskCategory, TaskInfo, TaskInfos } from "src/components/Tasks/TaskTypes"; import { UnchangedWarning } from "src/components/Tasks/UnchangedWarning"; -import { post } from "src/lib/api"; +import { useTaskContext } from "src/context/TaskContext"; import { getTypeSafei18nKey } from "src/lib/i18n"; +import { TaskCategory, TaskInfo } from "src/types/Task"; import { BaseTask, TaskContent, TaskReplyValidity } from "src/types/Task"; -import { CreateTaskType, KnownTaskType, LabelTaskType, RankTaskType } from "src/types/Tasks"; -import useSWRMutation from "swr/mutation"; +import { CreateTaskType, LabelTaskType, RankTaskType } from "src/types/Tasks"; interface EditMode { mode: "EDIT"; @@ -63,16 +62,11 @@ export interface TaskSurveyProps { onValidityChanged: (validity: TaskReplyValidity) => void; } -interface TaskProps { - frontendId: string; - task: KnownTaskType; - isLoading: boolean; - completeTask: (TaskContent) => void; - skipTask: () => void; -} - -export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: TaskProps) => { +export const Task = () => { const { t } = useTranslation("tasks"); + const rootEl = useRef(null); + const replyContent = useRef(null); + const { rejectTask, completeTask, isLoading, task, taskInfo } = useTaskContext(); const [taskStatus, taskEvent] = useReducer( ( status: TaskStatus, @@ -114,7 +108,6 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta { mode: "EDIT", replyValidity: "INVALID" } ); - const replyContent = useRef(null); const updateValidity = useCallback( (replyValidity: TaskReplyValidity) => taskEvent({ action: "UPDATE_VALIDITY", replyValidity }), [taskEvent] @@ -122,26 +115,7 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta useEffect(() => { taskEvent({ action: "NEW_TASK" }); - }, [task.id, updateValidity]); - - const rootEl = useRef(null); - - const taskType = useMemo(() => { - return TaskInfos.find((taskType) => taskType.type === task.type); - }, [task.type]); - - const { trigger: sendRejection } = useSWRMutation("/api/reject_task", post, { - onSuccess: async () => { - skipTask(); - }, - }); - - const rejectTask = (reason: string) => { - sendRejection({ - id: frontendId, - reason, - }); - }; + }, [task.id]); const onReplyChanged = useCallback( (content: TaskContent) => { @@ -150,25 +124,21 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta [replyContent] ); - const submitResponse = () => { + const submitResponse = useCallback(async () => { if (taskStatus.mode === "REVIEW") { - completeTask({ - id: frontendId, - update_type: taskType.update_type, - content: replyContent.current, - }); taskEvent({ action: "SET_SUBMITTED" }); + await completeTask(replyContent.current); scrollToTop(rootEl.current); } - }; + }, [taskStatus.mode, completeTask]); const taskTypeComponent = useMemo(() => { - switch (taskType.category) { + switch (taskInfo.category) { case TaskCategory.Create: return ( ); } - }, [task, taskType, taskStatus.mode, onReplyChanged, updateValidity]); + }, [taskInfo, task, taskStatus.mode, onReplyChanged, updateValidity]); return (
@@ -214,8 +184,8 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta /> taskEvent({ action: "RETURN_EDIT" })} onContinueAnyway={() => { diff --git a/website/src/components/Tasks/TaskHeader/TaskHeader.tsx b/website/src/components/Tasks/TaskHeader/TaskHeader.tsx index c4095eb8..65284898 100644 --- a/website/src/components/Tasks/TaskHeader/TaskHeader.tsx +++ b/website/src/components/Tasks/TaskHeader/TaskHeader.tsx @@ -1,8 +1,8 @@ import { HStack, IconButton, Link, Stack, Text, useColorModeValue } from "@chakra-ui/react"; import { HelpCircle } from "lucide-react"; import { useTranslation } from "next-i18next"; -import type { TaskInfo } from "src/components/Tasks/TaskTypes"; import { getTypeSafei18nKey } from "src/lib/i18n"; +import { TaskInfo } from "src/types/Task"; interface TaskHeaderProps { /** diff --git a/website/src/components/Tasks/TaskTypes.tsx b/website/src/components/Tasks/TaskTypes.tsx index e2a53d23..4f204720 100644 --- a/website/src/components/Tasks/TaskTypes.tsx +++ b/website/src/components/Tasks/TaskTypes.tsx @@ -1,28 +1,4 @@ -import { TaskType } from "src/types/Task"; - -export enum TaskCategory { - Create = "Create", - Evaluate = "Evaluate", - Label = "Label", - Random = "Random", -} - -export enum TaskUpdateType { - MessageRanking = "message_ranking", - Random = "random", - TextLabels = "text_labels", - TextReplyToMessage = "text_reply_to_message", -} - -export interface TaskInfo { - category: TaskCategory; - help_link: string; - id: string; - mode?: string; - pathname: string; - type: string; - update_type: string; -} +import { TaskCategory, TaskInfo, TaskType, TaskUpdateType } from "src/types/Task"; export const TaskCategoryLabels: { [key in TaskCategory]: string } = { [TaskCategory.Random]: "grab_a_task", diff --git a/website/src/context/TaskContext.ts b/website/src/context/TaskContext.ts new file mode 100644 index 00000000..dfbf145d --- /dev/null +++ b/website/src/context/TaskContext.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; +import { TaskApiHook } from "src/types/Hooks"; +import { BaseTask, TaskInfo } from "src/types/Task"; + +export interface TaskContextType + extends Omit, "response"> { + task: Task; + taskInfo: TaskInfo; +} + +export const TaskContext = createContext>(null); + +export const useTaskContext = () => useContext(TaskContext); diff --git a/website/src/hooks/tasks/useGenericTaskAPI.tsx b/website/src/hooks/tasks/useGenericTaskAPI.tsx index fc09367b..7a7d651b 100644 --- a/website/src/hooks/tasks/useGenericTaskAPI.tsx +++ b/website/src/hooks/tasks/useGenericTaskAPI.tsx @@ -1,38 +1,75 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; +import { TaskInfos } from "src/components/Tasks/TaskTypes"; import { get, post } from "src/lib/api"; import { TaskApiHook } from "src/types/Hooks"; -import { BaseTask, TaskAvailableResponse, TaskResponse, TaskType as TaskTypeEnum } from "src/types/Task"; +import { BaseTask, ServerTaskResponse, TaskResponse, TaskType as TaskTypeEnum } from "src/types/Task"; import useSWRImmutable from "swr/immutable"; import useSWRMutation from "swr/mutation"; -export const useGenericTaskAPI = (taskType: TaskTypeEnum): TaskApiHook => { - const [response, setReponse] = useState>({ taskAvailability: "AWAITING_INITIAL" }); +// TODO: provide type for the content reply, this will be much harder since the replies vary vastly +export const useGenericTaskAPI = ( + taskType: TaskTypeEnum +): TaskApiHook => { + const [response, setResponse] = useState>({ taskAvailability: "AWAITING_INITIAL" }); + // Note: We use isValidating to indiate we are loading beause it signals eash load, not just the first one. - const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable>( + const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable>( "/api/new_task/" + taskType, get, { - onSuccess: (response) => { - setReponse({ taskAvailability: "AVAILABLE", ...response }); + onSuccess: (taskResponse) => { + setResponse({ + ...taskResponse, + taskAvailability: "AVAILABLE", + taskInfo: TaskInfos.find((taskType) => taskType.type === taskResponse.task.type), + }); }, onError: () => { - // We could check for code 503 here for truely unavailable, but we need to do something with other errors anyway. - setReponse({ taskAvailability: "NONE_AVAILABLE" }); + // We could check for code 503 here for truly unavailable, but we need to do something with other errors anyway. + setResponse({ taskAvailability: "NONE_AVAILABLE" }); }, revalidateOnMount: true, dedupingInterval: 500, } ); - const { trigger: completeTask } = useSWRMutation>("/api/update_task", post, { + const { trigger: sendTaskContent } = useSWRMutation("/api/update_task", post, { onSuccess: () => { requestNewTask(); }, onError: () => { - // We could check for code 503 here for truely unavailable, but we need to do something with other errors anyway. - setReponse({ taskAvailability: "NONE_AVAILABLE" }); + // We could check for code 503 here for truly unavailable, but we need to do something with other errors anyway. + setResponse({ taskAvailability: "NONE_AVAILABLE" }); }, }); - return { response, isLoading, completeTask, skipTask: requestNewTask }; + // NOTE: it might make sense to split this hook into 2 parts + + // makes sure that requestNewTask is always called without parameters: + const skipTask = useCallback(async () => { + await requestNewTask(); + }, [requestNewTask]); + + const { trigger: sendRejection } = useSWRMutation("/api/reject_task", post, { onSuccess: skipTask }); + const rejectTask = useCallback( + async (reason: string) => { + if (response.taskAvailability !== "AVAILABLE") { + throw new Error("Cannot reject task that is not yet ready"); + } + await sendRejection({ id: response.id, reason }); + }, + [response, sendRejection] + ); + + const completeTask = useCallback( + async (content: ResponseContent) => { + if (response.taskAvailability !== "AVAILABLE") { + throw new Error("Cannot complete task that is not yet ready"); + } + await sendTaskContent({ id: response.id, update_type: response.taskInfo.update_type, content }); + }, + [response, sendTaskContent] + ); + + return { response, isLoading, rejectTask, completeTask, skipTask }; }; diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx index dbba3c8a..23fcec06 100644 --- a/website/src/pages/dashboard.tsx +++ b/website/src/pages/dashboard.tsx @@ -4,9 +4,8 @@ import { useTranslation } from "next-i18next"; import { useEffect, useMemo, useState } from "react"; import { LeaderboardWidget, TaskOption, WelcomeCard } from "src/components/Dashboard"; import { getDashboardLayout } from "src/components/Layout"; -import { TaskCategory } from "src/components/Tasks/TaskTypes"; import { get } from "src/lib/api"; -import { AvailableTasks, TaskType } from "src/types/Task"; +import { AvailableTasks, TaskCategory, TaskType } from "src/types/Task"; export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props"; import useSWR from "swr"; diff --git a/website/src/types/Hooks.ts b/website/src/types/Hooks.ts index 8da40527..acdd0b58 100644 --- a/website/src/types/Hooks.ts +++ b/website/src/types/Hooks.ts @@ -1,16 +1,11 @@ -import { BaseTask, TaskContent, TaskResponse, TaskType } from "src/types/Task"; +import { BaseTask, TaskResponse, TaskType } from "src/types/Task"; -interface TaskInteraction { - id: string; - update_type: string; - content: TaskContent; -} - -export type TaskApiHook = { +export type TaskApiHook = { response: TaskResponse; isLoading: boolean; - completeTask: (interaction: TaskInteraction) => void; - skipTask: () => void; + completeTask: (interaction: ResponseContent) => Promise; + skipTask: () => Promise; + rejectTask: (reason: string) => Promise; }; -export type TaskApiHooks = Record TaskApiHook>; +export type TaskApiHooks = Record TaskApiHook>; diff --git a/website/src/types/Task.ts b/website/src/types/Task.ts index 5e2abf16..cb493b56 100644 --- a/website/src/types/Task.ts +++ b/website/src/types/Task.ts @@ -14,6 +14,30 @@ export enum TaskType { random = "random", } +export enum TaskCategory { + Create = "Create", + Evaluate = "Evaluate", + Label = "Label", + Random = "Random", +} + +export interface TaskInfo { + category: TaskCategory; + help_link: string; + id: string; + mode?: string; + pathname: string; + type: string; + update_type: string; +} + +export enum TaskUpdateType { + MessageRanking = "message_ranking", + Random = "random", + TextLabels = "text_labels", + TextReplyToMessage = "text_reply_to_message", +} + // we need to reconsider how to handle task content types // eslint-disable-next-line @typescript-eslint/no-explicit-any export type TaskContent = any; @@ -29,14 +53,15 @@ export interface BaseTask { type: TaskType; } -export interface TaskAvailableResponse { +export interface ServerTaskResponse { id: string; userId: string; task: Task; } -interface TaskAvailable extends TaskAvailableResponse { +interface TaskAvailable extends ServerTaskResponse { taskAvailability: "AVAILABLE"; + taskInfo: TaskInfo; } interface AwaitingInitialTask {