From a19e0fa085d3110c956b1375ffe7162920b59d22 Mon Sep 17 00:00:00 2001 From: AbdBarho Date: Sun, 8 Jan 2023 09:21:07 +0100 Subject: [PATCH 1/4] Extract generic code out of labeling task --- .../src/components/Loading/LoadingScreen.jsx | 2 +- website/src/components/Tasks/LabelTask.tsx | 100 +++++++++++++++ website/src/hooks/tasks/useGenericTaskAPI.tsx | 42 +++++++ .../src/hooks/tasks/useLabelInitialPrompt.tsx | 27 ++++ website/src/hooks/useLabelingTask.ts | 52 -------- .../src/pages/label/label_initial_prompt.tsx | 117 ++++-------------- 6 files changed, 191 insertions(+), 149 deletions(-) create mode 100644 website/src/components/Tasks/LabelTask.tsx create mode 100644 website/src/hooks/tasks/useGenericTaskAPI.tsx create mode 100644 website/src/hooks/tasks/useLabelInitialPrompt.tsx delete mode 100644 website/src/hooks/useLabelingTask.ts diff --git a/website/src/components/Loading/LoadingScreen.jsx b/website/src/components/Loading/LoadingScreen.jsx index 02aabe7a..3595b3c4 100644 --- a/website/src/components/Loading/LoadingScreen.jsx +++ b/website/src/components/Loading/LoadingScreen.jsx @@ -1,7 +1,7 @@ import { Progress } from "@chakra-ui/react"; import { useColorMode } from "@chakra-ui/react"; -export const LoadingScreen = ({ text }) => { +export const LoadingScreen = ({ text = "Loading..." } = {}) => { const { colorMode } = useColorMode(); const mainClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white"; diff --git a/website/src/components/Tasks/LabelTask.tsx b/website/src/components/Tasks/LabelTask.tsx new file mode 100644 index 00000000..bb9d417c --- /dev/null +++ b/website/src/components/Tasks/LabelTask.tsx @@ -0,0 +1,100 @@ +import { Grid, Slider, SliderFilledTrack, SliderThumb, SliderTrack } from "@chakra-ui/react"; +import { useColorMode } from "@chakra-ui/react"; +import { ReactNode, useEffect, useId, useMemo, useState } from "react"; +import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards"; +import { colors } from "styles/Theme/colors"; + +export const LabelTask = ({ + title, + desc, + messages, + inputs, + controls, +}: { + title: string; + desc: string; + messages: ReactNode; + inputs: ReactNode; + controls: ReactNode; +}) => { + const { colorMode } = useColorMode(); + const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white"; + + const card = useMemo( + () => ( + <> +
{title}
+

{desc}

+ {messages} + + ), + [title, desc, messages] + ); + + return ( +
+ + {card} + {inputs} + + {controls} +
+ ); +}; + +// TODO: consolidate with FlaggableElement +interface LabelSliderGroupProps { + labelIDs: Array; + onChange: (sliderValues: number[]) => unknown; +} + +export const LabelSliderGroup = ({ labelIDs, onChange }: LabelSliderGroupProps) => { + const [sliderValues, setSliderValues] = useState(Array.from({ length: labelIDs.length }).map(() => 0)); + + useEffect(() => { + onChange(sliderValues); + }, [sliderValues, onChange]); + + return ( + + {labelIDs.map((labelId, idx) => ( + { + const newState = sliderValues.slice(); + newState[idx] = sliderValue; + setSliderValues(newState); + }} + /> + ))} + + ); +}; + +function CheckboxSliderItem(props: { + labelId: string; + sliderValue: number; + sliderHandler: (newVal: number) => unknown; +}) { + const id = useId(); + const { colorMode } = useColorMode(); + + const labelTextClass = colorMode === "light" ? `text-${colors.light.text}` : `text-${colors.dark.text}`; + + return ( + <> + + props.sliderHandler(val / 100)}> + + + + + + + ); +} diff --git a/website/src/hooks/tasks/useGenericTaskAPI.tsx b/website/src/hooks/tasks/useGenericTaskAPI.tsx new file mode 100644 index 00000000..1a6c0be9 --- /dev/null +++ b/website/src/hooks/tasks/useGenericTaskAPI.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from "react"; +import fetcher from "src/lib/fetcher"; +import poster from "src/lib/poster"; +import useSWRImmutable from "swr/immutable"; +import useSWRMutation from "swr/mutation"; + +// TODO: type & centralize types for all tasks + +export interface TaskResponse { + id: string; + userId: string; + task: TaskType; +} + +export const useGenericTaskAPI = (taskApiEndpoint: string) => { + type ConcreteTaskResponse = TaskResponse; + + const [tasks, setTasks] = useState([]); + + const { isLoading, mutate, error } = useSWRImmutable( + "/api/new_task/" + taskApiEndpoint, + fetcher, + { + onSuccess: (data) => setTasks([data]), + } + ); + + useEffect(() => { + if (tasks.length === 0 && !isLoading && !error) { + mutate(); + } + }, [tasks, isLoading, mutate, error]); + + const { trigger } = useSWRMutation("/api/update_task", poster, { + onSuccess: async (response) => { + const newTask: ConcreteTaskResponse = await response.json(); + setTasks((oldTasks) => [...oldTasks, newTask]); + }, + }); + + return { tasks, isLoading, trigger, error, reset: mutate }; +}; diff --git a/website/src/hooks/tasks/useLabelInitialPrompt.tsx b/website/src/hooks/tasks/useLabelInitialPrompt.tsx new file mode 100644 index 00000000..5d6ca372 --- /dev/null +++ b/website/src/hooks/tasks/useLabelInitialPrompt.tsx @@ -0,0 +1,27 @@ +import { TaskResponse, useGenericTaskAPI } from "./useGenericTaskAPI"; + +export interface LabelInitialPromptTask { + id: string; + type: "label_initial_prompt"; + message_id: string; + valid_labels: string[]; + prompt: string; +} + +export type LabelInitialPromptTaskResponse = TaskResponse; + +export const useLabelInitialPromptTask = () => { + const { tasks, isLoading, trigger, reset, error } = useGenericTaskAPI("label_initial_prompt"); + + const submit = (id: string, message_id: string, text: string, validLabels: string[], labelWeights: number[]) => { + console.assert(validLabels.length === labelWeights.length); + const labels = validLabels.reduce( + (obj, label, i) => ((obj[label] = labelWeights[i]), obj), + {} as Record + ); + + return trigger({ id, update_type: "text_labels", content: { labels, text, message_id } }); + }; + + return { tasks, isLoading, submit, reset, error }; +}; diff --git a/website/src/hooks/useLabelingTask.ts b/website/src/hooks/useLabelingTask.ts deleted file mode 100644 index 872909b7..00000000 --- a/website/src/hooks/useLabelingTask.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useEffect, useState } from "react"; -import fetcher from "src/lib/fetcher"; -import poster from "src/lib/poster"; -import useSWRImmutable from "swr/immutable"; -import useSWRMutation from "swr/mutation"; - -// TODO: type & centralize types for all tasks -interface TaskResponse { - id: string; - userId: string; - task: TaskType; -} - -export interface LabelInitialPromptTask { - id: string; - message_id: string; - prompt: string; - type: string; - valid_labels: string[]; -} - -export type LabelInitialPromptTaskResponse = TaskResponse; - -export const useLabelingTask = ({ taskApiEndpoint }: { taskApiEndpoint: "label_initial_prompt" }) => { - type ConcreteTaskResponse = TaskResponse; - - const [tasks, setTasks] = useState>([]); - - const { isLoading, mutate, error } = useSWRImmutable("/api/new_task/" + taskApiEndpoint, fetcher, { - onSuccess: (data: ConcreteTaskResponse) => { - setTasks([data]); - }, - }); - - useEffect(() => { - if (tasks.length === 0 && !isLoading && !error) { - mutate(); - } - }, [tasks, isLoading, mutate, error]); - - const { trigger } = useSWRMutation("/api/update_task", poster, { - onSuccess: async (reply) => { - const newTask: ConcreteTaskResponse = await reply.json(); - setTasks((oldTasks) => [...oldTasks, newTask]); - }, - }); - - const submit = (id: string, message_id: string, text: string, labels: Record) => - trigger({ id, update_type: "text_labels", content: { labels, text, message_id } }); - - return { tasks, isLoading, submit, error, reset: mutate }; -}; diff --git a/website/src/pages/label/label_initial_prompt.tsx b/website/src/pages/label/label_initial_prompt.tsx index e400e8fd..346362dc 100644 --- a/website/src/pages/label/label_initial_prompt.tsx +++ b/website/src/pages/label/label_initial_prompt.tsx @@ -1,113 +1,38 @@ -import { Container, Grid, Slider, SliderFilledTrack, SliderThumb, SliderTrack } from "@chakra-ui/react"; -import { useColorMode } from "@chakra-ui/react"; -import { useEffect, useId, useState } from "react"; +import { useState } from "react"; import { LoadingScreen } from "src/components/Loading/LoadingScreen"; import { MessageView } from "src/components/Messages"; import { TaskControls } from "src/components/Survey/TaskControls"; -import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards"; -import { LabelInitialPromptTask, LabelInitialPromptTaskResponse, useLabelingTask } from "src/hooks/useLabelingTask"; -import { colors } from "styles/Theme/colors"; +import { LabelSliderGroup, LabelTask } from "src/components/Tasks/LabelTask"; +import { LabelInitialPromptTaskResponse, useLabelInitialPromptTask } from "src/hooks/tasks/useLabelInitialPrompt"; const LabelInitialPrompt = () => { const [sliderValues, setSliderValues] = useState([]); - const { tasks, isLoading, submit, reset } = useLabelingTask({ - taskApiEndpoint: "label_initial_prompt", - }); + const { tasks, isLoading, submit, reset } = useLabelInitialPromptTask(); - const submitResponse = ({ id, task }: LabelInitialPromptTaskResponse) => { - const labels = task.valid_labels.reduce((obj, label, i) => { - obj[label] = sliderValues[i].toString(); - return obj; - }, {} as Record); - - submit(id, task.message_id, task.prompt, labels); - }; - - const { colorMode } = useColorMode(); - const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white"; - - if (isLoading) { - return ; - } - - if (tasks.length === 0) { - return No tasks found...; + if (isLoading || tasks.length === 0) { + return ; } const task = tasks[0].task; return ( -
- - <> -
Label Initial Prompt
-

Provide labels for the following prompt

- - - -
- -
+ } + inputs={} + controls={ + + submit(id, task.message_id, task.prompt, task.valid_labels, sliderValues) + } + /> + } + /> ); }; export default LabelInitialPrompt; - -// TODO: consolidate with FlaggableElement - -interface CheckboxSliderGroupProps { - labelIDs: Array; - onChange: (sliderValues: number[]) => unknown; -} - -const CheckboxSliderGroup = ({ labelIDs, onChange }: CheckboxSliderGroupProps) => { - const [sliderValues, setSliderValues] = useState(Array.from({ length: labelIDs.length }).map(() => 0)); - - useEffect(() => { - onChange(sliderValues); - }, [sliderValues, onChange]); - - return ( - - {labelIDs.map((labelId, idx) => ( - { - const newState = sliderValues.slice(); - newState[idx] = sliderValue; - setSliderValues(newState); - }} - /> - ))} - - ); -}; - -function CheckboxSliderItem(props: { - labelId: string; - sliderValue: number; - sliderHandler: (newVal: number) => unknown; -}) { - const id = useId(); - const { colorMode } = useColorMode(); - - const labelTextClass = colorMode === "light" ? `text-${colors.light.text}` : `text-${colors.dark.text}`; - - return ( - <> - - props.sliderHandler(val / 100)}> - - - - - - - ); -} From f7dceee87a56c6d828b7ee4739f260d01e8738d1 Mon Sep 17 00:00:00 2001 From: AbdBarho Date: Sun, 8 Jan 2023 09:49:50 +0100 Subject: [PATCH 2/4] Add LabelPrompterReplyTask --- website/src/components/Messages.tsx | 3 +- website/src/components/Tasks/TaskTypes.tsx | 7 +++ .../src/hooks/tasks/useLabelInitialPrompt.tsx | 5 +-- .../src/hooks/tasks/useLabelPrompterReply.ts | 30 +++++++++++++ website/src/middleware.ts | 4 +- .../src/pages/label/label_prompter_reply.tsx | 44 +++++++++++++++++++ 6 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 website/src/hooks/tasks/useLabelPrompterReply.ts create mode 100644 website/src/pages/label/label_prompter_reply.tsx diff --git a/website/src/components/Messages.tsx b/website/src/components/Messages.tsx index 226c6154..fb84559e 100644 --- a/website/src/components/Messages.tsx +++ b/website/src/components/Messages.tsx @@ -12,8 +12,7 @@ export interface Message { export const Messages = ({ messages, post_id }: { messages: Message[]; post_id: string }) => { const items = messages.map((messageProps: Message, i: number) => { - const { message_id } = messageProps; - const { text } = messageProps; + const { message_id, text } = messageProps; return ( diff --git a/website/src/components/Tasks/TaskTypes.tsx b/website/src/components/Tasks/TaskTypes.tsx index 7cec2177..09e106b6 100644 --- a/website/src/components/Tasks/TaskTypes.tsx +++ b/website/src/components/Tasks/TaskTypes.tsx @@ -63,4 +63,11 @@ export const TaskTypes = [ pathname: "/label/label_initial_prompt", type: "label_initial_prompt", }, + { + label: "Label Prompter Reply", + desc: "Provide labels for a prompt.", + category: TaskCategory.Label, + pathname: "/label/label_prompter_reply", + type: "label_prompter_reply", + }, ]; diff --git a/website/src/hooks/tasks/useLabelInitialPrompt.tsx b/website/src/hooks/tasks/useLabelInitialPrompt.tsx index 5d6ca372..69ab4bcc 100644 --- a/website/src/hooks/tasks/useLabelInitialPrompt.tsx +++ b/website/src/hooks/tasks/useLabelInitialPrompt.tsx @@ -15,10 +15,7 @@ export const useLabelInitialPromptTask = () => { const submit = (id: string, message_id: string, text: string, validLabels: string[], labelWeights: number[]) => { console.assert(validLabels.length === labelWeights.length); - const labels = validLabels.reduce( - (obj, label, i) => ((obj[label] = labelWeights[i]), obj), - {} as Record - ); + const labels = Object.fromEntries(validLabels.map((label, i) => [label, labelWeights[i]])); return trigger({ id, update_type: "text_labels", content: { labels, text, message_id } }); }; diff --git a/website/src/hooks/tasks/useLabelPrompterReply.ts b/website/src/hooks/tasks/useLabelPrompterReply.ts new file mode 100644 index 00000000..9b7a61da --- /dev/null +++ b/website/src/hooks/tasks/useLabelPrompterReply.ts @@ -0,0 +1,30 @@ +import { TaskResponse, useGenericTaskAPI } from "./useGenericTaskAPI"; + +export interface LabelPrompterReplyTask { + id: string; + type: "label_prompter_reply"; + message_id: string; + valid_labels: string[]; + reply: string; + conversation: { + messages: Array<{ + text: string; + is_assistant: boolean; + }>; + }; +} + +export type LabelPrompterReplyTaskResponse = TaskResponse; + +export const useLabelPrompterReplyTask = () => { + const { tasks, isLoading, trigger, reset, error } = useGenericTaskAPI("label_prompter_reply"); + + const submit = (id: string, message_id: string, text: string, validLabels: string[], labelWeights: number[]) => { + console.assert(validLabels.length === labelWeights.length); + const labels = Object.fromEntries(validLabels.map((label, i) => [label, labelWeights[i]])); + + return trigger({ id, update_type: "text_labels", content: { labels, text, message_id } }); + }; + + return { tasks, isLoading, submit, reset, error }; +}; diff --git a/website/src/middleware.ts b/website/src/middleware.ts index b6a539b4..d1cd6801 100644 --- a/website/src/middleware.ts +++ b/website/src/middleware.ts @@ -1,8 +1,8 @@ export { default } from "next-auth/middleware"; /** - * Guards all pages under `/grading` and redirects them to the sign in page. + * Guards these pages and redirects them to the sign in page. */ export const config = { - matcher: ["/create/:path*", "/evaluate/:path*", "/account/:path*", "/dashboard"], + matcher: ["/create/:path*", "/evaluate/:path*", "/label/:path*", "/account/:path*", "/dashboard", "/admin/:path*"], }; diff --git a/website/src/pages/label/label_prompter_reply.tsx b/website/src/pages/label/label_prompter_reply.tsx new file mode 100644 index 00000000..743bde97 --- /dev/null +++ b/website/src/pages/label/label_prompter_reply.tsx @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { LoadingScreen } from "src/components/Loading/LoadingScreen"; +import { Message, Messages } from "src/components/Messages"; +import { TaskControls } from "src/components/Survey/TaskControls"; +import { LabelSliderGroup, LabelTask } from "src/components/Tasks/LabelTask"; +import { LabelPrompterReplyTaskResponse, useLabelPrompterReplyTask } from "src/hooks/tasks/useLabelPrompterReply"; + +const LabelPrompterReply = () => { + const [sliderValues, setSliderValues] = useState([]); + + const { tasks, isLoading, submit, reset } = useLabelPrompterReplyTask(); + + if (isLoading || tasks.length === 0) { + return ; + } + + const task = tasks[0].task; + const messages: Message[] = [ + // TODO: could we re-use the task message_id as message id for all messages in the conversation? + // or should we ask the backend team to send message ids in the task? + ...task.conversation.messages.map((m) => ({ ...m, message_id: null })), + { text: task.reply, is_assistant: false, message_id: task.message_id }, + ]; + + return ( + } + inputs={} + controls={ + + submit(id, task.message_id, task.reply, task.valid_labels, sliderValues) + } + /> + } + /> + ); +}; + +export default LabelPrompterReply; From fd9edf29d59221974cd416ec56ea54d2d9e9bf37 Mon Sep 17 00:00:00 2001 From: AbdBarho Date: Sun, 8 Jan 2023 10:52:48 +0100 Subject: [PATCH 3/4] Use MessageTable --- website/src/pages/label/label_prompter_reply.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/src/pages/label/label_prompter_reply.tsx b/website/src/pages/label/label_prompter_reply.tsx index 743bde97..81be8fc3 100644 --- a/website/src/pages/label/label_prompter_reply.tsx +++ b/website/src/pages/label/label_prompter_reply.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { LoadingScreen } from "src/components/Loading/LoadingScreen"; -import { Message, Messages } from "src/components/Messages"; +import { Message } from "src/components/Messages"; +import { MessageTable } from "src/components/Messages/MessageTable"; import { TaskControls } from "src/components/Survey/TaskControls"; import { LabelSliderGroup, LabelTask } from "src/components/Tasks/LabelTask"; import { LabelPrompterReplyTaskResponse, useLabelPrompterReplyTask } from "src/hooks/tasks/useLabelPrompterReply"; @@ -26,7 +27,7 @@ const LabelPrompterReply = () => { } + messages={} inputs={} controls={ Date: Sun, 8 Jan 2023 10:58:24 +0100 Subject: [PATCH 4/4] Updating task schema according to backend --- website/src/hooks/tasks/useLabelPrompterReply.ts | 1 + website/src/pages/label/label_prompter_reply.tsx | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/website/src/hooks/tasks/useLabelPrompterReply.ts b/website/src/hooks/tasks/useLabelPrompterReply.ts index 9b7a61da..f048c2b3 100644 --- a/website/src/hooks/tasks/useLabelPrompterReply.ts +++ b/website/src/hooks/tasks/useLabelPrompterReply.ts @@ -10,6 +10,7 @@ export interface LabelPrompterReplyTask { messages: Array<{ text: string; is_assistant: boolean; + message_id: string; }>; }; } diff --git a/website/src/pages/label/label_prompter_reply.tsx b/website/src/pages/label/label_prompter_reply.tsx index 81be8fc3..44606f47 100644 --- a/website/src/pages/label/label_prompter_reply.tsx +++ b/website/src/pages/label/label_prompter_reply.tsx @@ -17,9 +17,7 @@ const LabelPrompterReply = () => { const task = tasks[0].task; const messages: Message[] = [ - // TODO: could we re-use the task message_id as message id for all messages in the conversation? - // or should we ask the backend team to send message ids in the task? - ...task.conversation.messages.map((m) => ({ ...m, message_id: null })), + ...task.conversation.messages, { text: task.reply, is_assistant: false, message_id: task.message_id }, ];