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/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/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/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 new file mode 100644 index 00000000..6743b33a --- /dev/null +++ b/website/pages/api/new_task.js @@ -0,0 +1,71 @@ +import { unstable_getServerSession } from "next-auth/next"; +import { authOptions } from "pages/api/auth/[...nextauth]"; + +/** + * 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: { + "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(); + + // Store the task and link it to the user.. + const registeredTask = await prisma.registeredTask.create({ + data: { + task, + user: { + connect: { + id: session.user.id, + }, + }, + }, + }); + + // Update the backend with our Task 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(); + + // 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 new file mode 100644 index 00000000..45b1f72e --- /dev/null +++ b/website/pages/api/update_task.js @@ -0,0 +1,80 @@ +import { unstable_getServerSession } from "next-auth/next"; +import { authOptions } from "./auth/[...nextauth]"; + +/** + * 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, + task: { + connect: { + id, + }, + }, + }, + }); + + // 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`, + { + method: "POST", + headers: { + "X-API-Key": process.env.FASTAPI_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "post_rating", + user: { + id: session.user.id, + display_name: session.user.name, + auth_method: "local", + }, + post_id: id, + user_post_id: interaction.id, + ...content, + }), + } + ); + const newTask = await interactionRes.json(); + + // 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/index.js b/website/pages/index.js index 022ba1dc..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); @@ -19,7 +19,6 @@ export default function Home() { return (
- {/* logo */}

Open Assistant

Open Assistant is a project meant to give everyone access to a great @@ -60,8 +59,6 @@ export default function Home() { return (

- {/* logo */} -

Open Assistant

You are logged in

diff --git a/website/pages/new_task.js b/website/pages/new_task.js new file mode 100644 index 00000000..77577662 --- /dev/null +++ b/website/pages/new_task.js @@ -0,0 +1,98 @@ +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); + +/** + * 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", + body: JSON.stringify(arg), + }); +} + +export default function NewPage() { + // 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); + + // 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(); + // This is the more efficient way to update a react state array. + setTasks((oldTasks) => [...oldTasks, newTask]); + }, + } + ); + + // Trigger a mutation that updates the current task. We should probably + // signal somewhere that this interaction is being processed. + const submitResponse = (t) => { + trigger({ + 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 ( +
+ {tasks.map((t) => ( +
+
{t.task.type}
+
{t.task.text}
+ {t.task.summary && ( + <> +
{t.task.summary}
+
+ {t.task.scale.min} to {t.task.scale.max} +
+ + + + )} +
+ ))} +
+ ); +} diff --git a/website/prisma/schema.prisma b/website/prisma/schema.prisma index 5ef3e0f0..9a9678c9 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,22 @@ 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 + + interaction TaskInteraction[] +} + +model TaskInteraction { + id String @id @default(uuid()) + + content Json + + task RegisteredTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + taskId String +}