Merge pull request #994 from LAION-AI/refactor

Consolidate API calls to TaskContext
This commit is contained in:
AbdBarho
2023-01-30 09:28:28 +01:00
committed by GitHub
11 changed files with 162 additions and 137 deletions
@@ -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<Record<TaskCategory, TaskType[]>>;
+27 -26
View File
@@ -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 <LoadingScreen text={t("common:loading")} />;
case "NONE_AVAILABLE":
return <TaskEmptyState />;
case "AVAILABLE": {
const { task, taskInfo } = response;
const context = { ...hookState, task, taskInfo };
return (
<TaskContext.Provider value={context}>
<Task key={response.id} />
</TaskContext.Provider>
);
}
}
}, [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 = <LoadingScreen text={t("common:loading")} />;
break;
case "NONE_AVAILABLE":
body = <TaskEmptyState />;
break;
case "AVAILABLE":
body = (
<Task
key={response.task.id}
frontendId={response.id}
task={response.task as KnownTaskType}
isLoading={isLoading}
completeTask={completeTask}
skipTask={skipTask}
/>
);
break;
}
return (
<>
<Head>
@@ -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 (
<Task frontendId={frontendId} task={task} isLoading={isLoading} completeTask={completeTask} skipTask={skipTask} />
<TaskContext.Provider value={providerValue}>
<Task />
</TaskContext.Provider>
);
};
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 };
+18 -48
View File
@@ -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<TaskType extends BaseTask, T> {
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<HTMLDivElement>(null);
const replyContent = useRef<TaskContent>(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<TaskContent>(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<HTMLDivElement>(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 (
<CreateTask
task={task as CreateTaskType}
taskType={taskType}
taskType={taskInfo}
isEditable={taskStatus.mode === "EDIT"}
isDisabled={taskStatus.mode === "SUBMITTED"}
onReplyChanged={onReplyChanged}
@@ -179,7 +149,7 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta
return (
<EvaluateTask
task={task as RankTaskType}
taskType={taskType}
taskType={taskInfo}
isEditable={taskStatus.mode === "EDIT"}
isDisabled={taskStatus.mode === "SUBMITTED"}
onReplyChanged={onReplyChanged}
@@ -190,7 +160,7 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta
return (
<LabelTask
task={task as LabelTaskType}
taskType={taskType}
taskType={taskInfo}
isEditable={taskStatus.mode === "EDIT"}
isDisabled={taskStatus.mode === "SUBMITTED"}
onReplyChanged={onReplyChanged}
@@ -198,7 +168,7 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta
/>
);
}
}, [task, taskType, taskStatus.mode, onReplyChanged, updateValidity]);
}, [taskInfo, task, taskStatus.mode, onReplyChanged, updateValidity]);
return (
<div ref={rootEl}>
@@ -214,8 +184,8 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta
/>
<UnchangedWarning
show={taskStatus.mode === "DEFAULT_WARN"}
title={t(getTypeSafei18nKey(`${taskType.id}.unchanged_title`)) || t("default.unchanged_title")}
message={t(getTypeSafei18nKey(`${taskType.id}.unchanged_message`)) || t("default.unchanged_message")}
title={t(getTypeSafei18nKey(`${taskInfo.id}.unchanged_title`)) || t("default.unchanged_title")}
message={t(getTypeSafei18nKey(`${taskInfo.id}.unchanged_message`)) || t("default.unchanged_message")}
continueButtonText={"Continue anyway"}
onClose={() => taskEvent({ action: "RETURN_EDIT" })}
onContinueAnyway={() => {
@@ -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 {
/**
+1 -25
View File
@@ -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",
+13
View File
@@ -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<Task extends BaseTask, ResponseContent>
extends Omit<TaskApiHook<Task, ResponseContent>, "response"> {
task: Task;
taskInfo: TaskInfo;
}
export const TaskContext = createContext<TaskContextType<BaseTask, unknown>>(null);
export const useTaskContext = () => useContext(TaskContext);
+50 -13
View File
@@ -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 extends BaseTask>(taskType: TaskTypeEnum): TaskApiHook<TaskType> => {
const [response, setReponse] = useState<TaskResponse<TaskType>>({ taskAvailability: "AWAITING_INITIAL" });
// TODO: provide type for the content reply, this will be much harder since the replies vary vastly
export const useGenericTaskAPI = <TaskType extends BaseTask, ResponseContent = any>(
taskType: TaskTypeEnum
): TaskApiHook<TaskType, ResponseContent> => {
const [response, setResponse] = useState<TaskResponse<TaskType>>({ 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<TaskAvailableResponse<TaskType>>(
const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable<ServerTaskResponse<TaskType>>(
"/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<TaskAvailableResponse<TaskType>>("/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 };
};
+1 -2
View File
@@ -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";
+6 -11
View File
@@ -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<Task extends BaseTask> = {
export type TaskApiHook<Task extends BaseTask, ResponseContent> = {
response: TaskResponse<Task>;
isLoading: boolean;
completeTask: (interaction: TaskInteraction) => void;
skipTask: () => void;
completeTask: (interaction: ResponseContent) => Promise<void>;
skipTask: () => Promise<void>;
rejectTask: (reason: string) => Promise<void>;
};
export type TaskApiHooks = Record<TaskType, (args: TaskType) => TaskApiHook<BaseTask>>;
export type TaskApiHooks = Record<TaskType, (args: TaskType) => TaskApiHook<BaseTask, any>>;
+27 -2
View File
@@ -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<Task extends BaseTask> {
export interface ServerTaskResponse<Task extends BaseTask> {
id: string;
userId: string;
task: Task;
}
interface TaskAvailable<Task extends BaseTask> extends TaskAvailableResponse<Task> {
interface TaskAvailable<Task extends BaseTask> extends ServerTaskResponse<Task> {
taskAvailability: "AVAILABLE";
taskInfo: TaskInfo;
}
interface AwaitingInitialTask {