From 8617f5b2396bb51cd4ad217420d12cde5568ee89 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 17 Dec 2022 14:20:39 +0900 Subject: [PATCH 1/4] Implementing a bare bones interaction with the task backend --- website/pages/api/auth/[...nextauth].js | 41 ----------------- website/pages/api/new_task.js | 59 +++++++++++++++++++++++++ website/pages/api/update_task.js | 48 ++++++++++++++++++++ website/pages/index.js | 1 - website/pages/new_task.js | 53 ++++++++++++++++++++++ website/prisma/schema.prisma | 13 ++++-- 6 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 website/pages/api/new_task.js create mode 100644 website/pages/api/update_task.js create mode 100644 website/pages/new_task.js diff --git a/website/pages/api/auth/[...nextauth].js b/website/pages/api/auth/[...nextauth].js index 06044ac1..c4ff116c 100644 --- a/website/pages/api/auth/[...nextauth].js +++ b/website/pages/api/auth/[...nextauth].js @@ -36,47 +36,6 @@ export const authOptions = { return session; }, }, - events: { - /** - * When a new user signs in, we register them with the Labeler backend. - */ - async signIn({ user, account, profile, isNewUser }) { - if (!isNewUser) { - return; - } - try { - // Register the new user with the Labeler Backend. - const res = await fetch(`${process.env.FASTAPI_URL}/api/v1/labelers`, { - method: "POST", - headers: { - "X-API-Key": process.env.FASTAPI_KEY, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - discord_username: user.id, - display_name: user.name || user.email, - is_enabled: true, - notes: account.provider, - }), - }); - if (res.status !== 200) { - console.error(res.statusText); - return; - } - // Update the User entry with the Labeler Backend's ID so we can - // reference it later. - const { id: labelerId } = await res.json(); - await prisma.user.update({ - where: { id: user.id }, - data: { - labelerId, - }, - }); - } catch (error) { - console.error(error); - } - }, - }, }; export default NextAuth(authOptions); diff --git a/website/pages/api/new_task.js b/website/pages/api/new_task.js new file mode 100644 index 00000000..d9bd5012 --- /dev/null +++ b/website/pages/api/new_task.js @@ -0,0 +1,59 @@ +import { unstable_getServerSession } from "next-auth/next"; +import { authOptions } from "./auth/[...nextauth]"; + +/** + * Returns a list of prompts from the Labeler Backend. + */ +export default async (req, res) => { + const session = await unstable_getServerSession(req, res, authOptions); + + if (!session) { + res.status(401).end(); + return; + } + + const taskRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/tasks/`, { + method: "POST", + headers: { + "X-API-Key": process.env.FASTAPI_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "rate_summary", + user: { + id: session.user.id, + display_name: session.user.name, + auth_method: "local", + }, + }), + }); + const task = await taskRes.json(); + + const registeredTask = await prisma.registeredTask.create({ + data: { + task, + user: { + connect: { + id: session.user.id, + }, + }, + }, + }); + + const ackRes = await fetch( + `${process.env.FASTAPI_URL}/api/v1/tasks/${task.id}/ack`, + { + method: "POST", + headers: { + "X-API-Key": process.env.FASTAPI_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + post_id: registeredTask.id, + }), + } + ); + const ack = await ackRes.json(); + + res.status(200).json(registeredTask); +}; diff --git a/website/pages/api/update_task.js b/website/pages/api/update_task.js new file mode 100644 index 00000000..54044781 --- /dev/null +++ b/website/pages/api/update_task.js @@ -0,0 +1,48 @@ +import { unstable_getServerSession } from "next-auth/next"; +import { authOptions } from "./auth/[...nextauth]"; + +/** + * Returns a list of prompts from the Labeler Backend. + */ +export default async (req, res) => { + const session = await unstable_getServerSession(req, res, authOptions); + + if (!session) { + res.status(401).end(); + return; + } + + const { id, rating } = await JSON.parse(req.body); + + const registeredTask = await prisma.registeredTask.findUnique({ + where: { id }, + select: { task: true }, + }); + + const interactionRes = await fetch( + `${process.env.FASTAPI_URL}/api/v1/tasks/interaction`, + { + method: "POST", + headers: { + "X-API-Key": process.env.FASTAPI_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "text_reply_to_post", + user: { + id: session.user.id, + display_name: session.user.name, + auth_method: "local", + }, + post_id: id, + user_post_id: "1234", + text: rating, + }), + } + ); + console.log(interactionRes.status); + const interaction = await interactionRes.json(); + console.log(interaction); + + res.status(200).end(); +}; diff --git a/website/pages/index.js b/website/pages/index.js index d150d6c7..a5e186cb 100644 --- a/website/pages/index.js +++ b/website/pages/index.js @@ -19,7 +19,6 @@ export default function Home() { return (
- {/* logo */}

Open Chat Gpt

Open chat gpt is a project meant to give everyone access to a great diff --git a/website/pages/new_task.js b/website/pages/new_task.js new file mode 100644 index 00000000..a971f42c --- /dev/null +++ b/website/pages/new_task.js @@ -0,0 +1,53 @@ +import axios from "axios"; +import Head from "next/head"; +import Image from "next/image"; +import { useSession, signIn, signOut } from "next-auth/react"; +import { useEffect, useRef, useState } from "react"; +import useSWRImmutable from "swr/immutable"; +import useSWRMutation from "swr/mutation"; + +const fetcher = (url) => axios.get(url).then((res) => res.data); + +async function sendRequest(url, { arg }) { + return fetch(url, { + method: "POST", + body: JSON.stringify(arg), + }); +} + +export default function NewPage() { + const responseEl = useRef(null); + const { + data: registeredTask, + errors, + isLoading, + } = useSWRImmutable("/api/new_task", fetcher); + const { trigger, isMutating } = useSWRMutation( + "/api/update_task", + sendRequest + ); + + const submitResponse = () => { + trigger({ + id: registeredTask.id, + rating: responseEl.current.value, + }); + }; + if (isLoading) { + return

Loading
; + } + + return ( +
+
{registeredTask.id}
+
{registeredTask.task.type}
+
{registeredTask.task.text}
+
{registeredTask.task.summary}
+
+ {registeredTask.task.scale.min} to {registeredTask.task.scale.max} +
+ + +
+ ); +} diff --git a/website/prisma/schema.prisma b/website/prisma/schema.prisma index 5ef3e0f0..d8a4772f 100644 --- a/website/prisma/schema.prisma +++ b/website/prisma/schema.prisma @@ -41,11 +41,10 @@ model User { emailVerified DateTime? image String? - // Records the unique user id stored in the Labeler Backend. - labelerId Int? - accounts Account[] sessions Session[] + + tasks RegisteredTask[] } model VerificationToken { @@ -55,3 +54,11 @@ model VerificationToken { @@unique([identifier, token]) } + +model RegisteredTask { + id String @id @default(uuid()) + task Json + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String +} From 699d0a948c5b94d320c785d3edf385115ae9cf9b Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sat, 17 Dec 2022 22:51:20 +0900 Subject: [PATCH 2/4] Logging interactions to tasks --- .gitignore | 1 + website/pages/api/update_task.js | 26 ++++++++++++++++---------- website/pages/new_task.js | 15 +++++++++++++-- website/prisma/schema.prisma | 11 +++++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index eb3a37f7..ce7a9b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv *.pyc +*.swp diff --git a/website/pages/api/update_task.js b/website/pages/api/update_task.js index 54044781..f68cd709 100644 --- a/website/pages/api/update_task.js +++ b/website/pages/api/update_task.js @@ -12,11 +12,17 @@ export default async (req, res) => { return; } - const { id, rating } = await JSON.parse(req.body); + const { id, content } = await JSON.parse(req.body); - const registeredTask = await prisma.registeredTask.findUnique({ - where: { id }, - select: { task: true }, + const interaction = await prisma.taskInteraction.create({ + data: { + content, + task: { + connect: { + id, + }, + }, + }, }); const interactionRes = await fetch( @@ -28,21 +34,21 @@ export default async (req, res) => { "Content-Type": "application/json", }, body: JSON.stringify({ - type: "text_reply_to_post", + type: "post_rating", user: { id: session.user.id, display_name: session.user.name, auth_method: "local", }, post_id: id, - user_post_id: "1234", - text: rating, + user_post_id: interaction.id, + ...content, }), } ); console.log(interactionRes.status); - const interaction = await interactionRes.json(); - console.log(interaction); + const newTask = await interactionRes.json(); + console.log(newTask); - res.status(200).end(); + res.status(200).json(newTask); }; diff --git a/website/pages/new_task.js b/website/pages/new_task.js index a971f42c..5308323f 100644 --- a/website/pages/new_task.js +++ b/website/pages/new_task.js @@ -16,6 +16,7 @@ async function sendRequest(url, { arg }) { } export default function NewPage() { + const [done, setDone] = useState(false); const responseEl = useRef(null); const { data: registeredTask, @@ -24,13 +25,22 @@ export default function NewPage() { } = useSWRImmutable("/api/new_task", fetcher); const { trigger, isMutating } = useSWRMutation( "/api/update_task", - sendRequest + sendRequest, + { + onSuccess: async (data) => { + const newTask = await data.json(); + console.log(newTask); + setDone(true); + }, + } ); const submitResponse = () => { trigger({ id: registeredTask.id, - rating: responseEl.current.value, + content: { + rating: responseEl.current.value, + }, }); }; if (isLoading) { @@ -48,6 +58,7 @@ export default function NewPage() {
+ {done &&
Done!
} ); } diff --git a/website/prisma/schema.prisma b/website/prisma/schema.prisma index d8a4772f..9a9678c9 100644 --- a/website/prisma/schema.prisma +++ b/website/prisma/schema.prisma @@ -61,4 +61,15 @@ model RegisteredTask { user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String + + interaction TaskInteraction[] +} + +model TaskInteraction { + id String @id @default(uuid()) + + content Json + + task RegisteredTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + taskId String } From a53d69b68260edb53365d54283d78ee07afc2863 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sun, 18 Dec 2022 15:43:19 +0900 Subject: [PATCH 3/4] Deleting some unused API methods, storing interactions properly to the Task Backend, Adding explanatory comments, using a task queue in the client side --- website/pages/api/hello.js | 5 --- website/pages/api/new_task.js | 14 +++++- website/pages/api/prompts.js | 28 ------------ website/pages/api/update_task.js | 34 +++++++++++++-- website/pages/new_task.js | 74 +++++++++++++++++++++++--------- 5 files changed, 97 insertions(+), 58 deletions(-) delete mode 100644 website/pages/api/hello.js delete mode 100644 website/pages/api/prompts.js diff --git a/website/pages/api/hello.js b/website/pages/api/hello.js deleted file mode 100644 index aee21e9a..00000000 --- a/website/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction - -export default function handler(req, res) { - res.status(200).json({ name: "John Doe" }); -} diff --git a/website/pages/api/new_task.js b/website/pages/api/new_task.js index d9bd5012..790c3f23 100644 --- a/website/pages/api/new_task.js +++ b/website/pages/api/new_task.js @@ -2,16 +2,25 @@ import { unstable_getServerSession } from "next-auth/next"; import { authOptions } from "./auth/[...nextauth]"; /** - * Returns a list of prompts from the Labeler Backend. + * Returns a new task created from the Task Backend. We do a few things here: + * + * 1) Get the task from the backend and register the requesting user. + * 2) Store the task in our local database. + * 3) Send and Ack to the Task Backend with our local id for the task. + * 4) Return everything to the client. */ export default async (req, res) => { const session = await unstable_getServerSession(req, res, authOptions); + // Return nothing if the user isn't registered. if (!session) { res.status(401).end(); return; } + // Fetch the new task. + // + // This needs to be refactored into an easier to use library. const taskRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/tasks/`, { method: "POST", headers: { @@ -29,6 +38,7 @@ export default async (req, res) => { }); const task = await taskRes.json(); + // Store the task and link it to the user.. const registeredTask = await prisma.registeredTask.create({ data: { task, @@ -40,6 +50,7 @@ export default async (req, res) => { }, }); + // Update the backend with our Task ID const ackRes = await fetch( `${process.env.FASTAPI_URL}/api/v1/tasks/${task.id}/ack`, { @@ -55,5 +66,6 @@ export default async (req, res) => { ); const ack = await ackRes.json(); + // Send the results to the client. res.status(200).json(registeredTask); }; diff --git a/website/pages/api/prompts.js b/website/pages/api/prompts.js deleted file mode 100644 index 9d09641c..00000000 --- a/website/pages/api/prompts.js +++ /dev/null @@ -1,28 +0,0 @@ -import { unstable_getServerSession } from "next-auth/next"; -import { authOptions } from "./auth/[...nextauth]"; - -/** - * Returns a list of prompts from the Labeler Backend. - */ -export default async (req, res) => { - const session = await unstable_getServerSession(req, res, authOptions); - - if (!session) { - res.status(401).end(); - return; - } - try { - const promptRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/prompts`, { - headers: { - "X-API-Key": process.env.FASTAPI_KEY, - }, - }); - const prompts = await promptRes.json(); - - res.status(200).json(prompts); - } catch (error) { - console.error(error); - res.status(500); - } - res.end(); -}; diff --git a/website/pages/api/update_task.js b/website/pages/api/update_task.js index f68cd709..45b1f72e 100644 --- a/website/pages/api/update_task.js +++ b/website/pages/api/update_task.js @@ -2,18 +2,28 @@ import { unstable_getServerSession } from "next-auth/next"; import { authOptions } from "./auth/[...nextauth]"; /** - * Returns a list of prompts from the Labeler Backend. + * Stores the task interaction with the Task Backend and then returns the next task generated. + * + * This implicity does a few things: + * 1) Stores the answer with the Task Backend. + * 2) Records the new task in our local database. + * 3) (TODO) Acks the new task with our local task ID to the Task Backend. + * 4) Returns the newly created task to the client. */ export default async (req, res) => { const session = await unstable_getServerSession(req, res, authOptions); + // Return nothing if the user isn't registered. if (!session) { res.status(401).end(); return; } + // Parse out the local task ID and the interaction contents. const { id, content } = await JSON.parse(req.body); + // Log the interaction locally to create our user_post_id needed by the Task + // Backend. const interaction = await prisma.taskInteraction.create({ data: { content, @@ -25,6 +35,8 @@ export default async (req, res) => { }, }); + // Send the interaction to the Task Backend. This automatically fetches the + // next task in the sequence (or the done task). const interactionRes = await fetch( `${process.env.FASTAPI_URL}/api/v1/tasks/interaction`, { @@ -46,9 +58,23 @@ export default async (req, res) => { }), } ); - console.log(interactionRes.status); const newTask = await interactionRes.json(); - console.log(newTask); - res.status(200).json(newTask); + // Stores the new task with our database. + const newRegisteredTask = await prisma.registeredTask.create({ + data: { + task: newTask, + user: { + connect: { + id: session.user.id, + }, + }, + }, + }); + + // TODO: Ack the task with the Task Backend using the newly created local + // task ID. + + // Send the next task in the sequence to the client. + res.status(200).json(newRegisteredTask); }; diff --git a/website/pages/new_task.js b/website/pages/new_task.js index 5308323f..77577662 100644 --- a/website/pages/new_task.js +++ b/website/pages/new_task.js @@ -8,6 +8,10 @@ import useSWRMutation from "swr/mutation"; const fetcher = (url) => axios.get(url).then((res) => res.data); +/** + * A helper function to post updates to tasks. + * This ensures the content sent is serialized to JSON. + */ async function sendRequest(url, { arg }) { return fetch(url, { method: "POST", @@ -16,49 +20,79 @@ async function sendRequest(url, { arg }) { } export default function NewPage() { - const [done, setDone] = useState(false); + // Use an array of tasks that record the sequence of steps until a task is + // deemed complete. + const [tasks, setTasks] = useState([]); + + // A quick reference to the input element. This should be factored into the + // component doing the actual task rendering. const responseEl = useRef(null); - const { - data: registeredTask, - errors, - isLoading, - } = useSWRImmutable("/api/new_task", fetcher); + + // Fetch the very fist task. We can ignore everything except isLoading + // because the onSuccess handler will update `tasks` when ready. + const { isLoading } = useSWRImmutable("/api/new_task", fetcher, { + onSuccess: (data) => { + setTasks([data]); + }, + }); + + // Every time we submit an answer to the latest task, let the backend handle + // all the interactions then add the resulting task to the queue. This ends + // when we hit the done task. const { trigger, isMutating } = useSWRMutation( "/api/update_task", sendRequest, { onSuccess: async (data) => { const newTask = await data.json(); - console.log(newTask); - setDone(true); + // This is the more efficient way to update a react state array. + setTasks((oldTasks) => [...oldTasks, newTask]); }, } ); - const submitResponse = () => { + // Trigger a mutation that updates the current task. We should probably + // signal somewhere that this interaction is being processed. + const submitResponse = (t) => { trigger({ - id: registeredTask.id, + id: t.id, content: { rating: responseEl.current.value, }, }); }; + + // Show something informative while loading the first task. if (isLoading) { return
Loading
; } + // Iterate through each of the tasks and show it's contents, get a response to it, or show the done state. + // + // Right now this just works for the rating task. + // + // Displaying and fetching results for each task type should be factored into + // different components that handle the presentation and response structures. + // The results should be packaged into a single object with all the fields + // sent to the backend. return (
-
{registeredTask.id}
-
{registeredTask.task.type}
-
{registeredTask.task.text}
-
{registeredTask.task.summary}
-
- {registeredTask.task.scale.min} to {registeredTask.task.scale.max} -
- - - {done &&
Done!
} + {tasks.map((t) => ( +
+
{t.task.type}
+
{t.task.text}
+ {t.task.summary && ( + <> +
{t.task.summary}
+
+ {t.task.scale.min} to {t.task.scale.max} +
+ + + + )} +
+ ))}
); } From 02ec176670cd9fdfc989f31cf5093703e9691d25 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Sun, 18 Dec 2022 17:38:59 +0900 Subject: [PATCH 4/4] Adding a jsconfig to ensure all javascript imports can be absolute instead of relative --- website/jsconfig.json | 5 +++++ website/pages/api/new_task.js | 2 +- website/pages/index.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 website/jsconfig.json diff --git a/website/jsconfig.json b/website/jsconfig.json new file mode 100644 index 00000000..36aa1a4d --- /dev/null +++ b/website/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/website/pages/api/new_task.js b/website/pages/api/new_task.js index 790c3f23..6743b33a 100644 --- a/website/pages/api/new_task.js +++ b/website/pages/api/new_task.js @@ -1,5 +1,5 @@ import { unstable_getServerSession } from "next-auth/next"; -import { authOptions } from "./auth/[...nextauth]"; +import { authOptions } from "pages/api/auth/[...nextauth]"; /** * Returns a new task created from the Task Backend. We do a few things here: diff --git a/website/pages/index.js b/website/pages/index.js index f0cb441a..9df611b9 100644 --- a/website/pages/index.js +++ b/website/pages/index.js @@ -5,7 +5,7 @@ import { useSession, signIn, signOut } from "next-auth/react"; import { useEffect, useState } from "react"; import useSWR from "swr"; -import styles from "../styles/Home.module.css"; +import styles from "styles/Home.module.css"; const fetcher = (url) => axios.get(url).then((res) => res.data);