From 2aacc542d3c7f10cef1e3428ad8a84c0a48c31d4 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Mon, 19 Dec 2022 15:12:45 +0900 Subject: [PATCH 1/7] Integrate the new UI pages with the working backend for the rate_summary task. This also migrates some pages to properly link up authorization flows. Some cruft is still in --- website/src/components/Header.tsx | 44 +++++---- website/src/lib/fetcher.ts | 8 ++ website/src/lib/poster.ts | 8 ++ website/{ => src}/lib/prismadb.ts | 0 website/src/middleware.ts | 8 ++ website/src/pages/api/auth/[...nextauth].ts | 38 ++++++++ website/src/pages/api/new_task/[task_type].ts | 71 ++++++++++++++ website/src/pages/api/update_task.ts | 78 ++++++++++++++++ website/src/pages/auth/signin.tsx | 85 +++++++++++++++++ website/src/pages/grading/grade-output.tsx | 93 ++++++++++++++----- website/src/pages/index.tsx | 22 +++-- website/src/pages/login.tsx | 55 ----------- .../src/pages/{new_task.js => new_task.tsx} | 5 +- 13 files changed, 412 insertions(+), 103 deletions(-) create mode 100644 website/src/lib/fetcher.ts create mode 100644 website/src/lib/poster.ts rename website/{ => src}/lib/prismadb.ts (100%) create mode 100644 website/src/middleware.ts create mode 100644 website/src/pages/api/auth/[...nextauth].ts create mode 100644 website/src/pages/api/new_task/[task_type].ts create mode 100644 website/src/pages/api/update_task.ts create mode 100644 website/src/pages/auth/signin.tsx delete mode 100644 website/src/pages/login.tsx rename website/src/pages/{new_task.js => new_task.tsx} (95%) diff --git a/website/src/components/Header.tsx b/website/src/components/Header.tsx index d22373b4..95fdf797 100644 --- a/website/src/components/Header.tsx +++ b/website/src/components/Header.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { signOut, useSession } from "next-auth/react"; import { Popover } from "@headlessui/react"; import { AnimatePresence, motion } from "framer-motion"; @@ -35,6 +36,31 @@ function MobileNavLink({ children, ...props }) { ); } +function AccountButton() { + const { data: session } = useSession(); + if (session) { + return ( + + ); + } + return ( + + + + ); +} + export function Header() { return (
@@ -93,14 +119,7 @@ export function Header() { FAQs
- - - +
@@ -109,14 +128,7 @@ export function Header() { )} - - - + diff --git a/website/src/lib/fetcher.ts b/website/src/lib/fetcher.ts new file mode 100644 index 00000000..3daa92a1 --- /dev/null +++ b/website/src/lib/fetcher.ts @@ -0,0 +1,8 @@ +import axios from "axios"; + +/** + * A minimal Axios based fetcher. + */ +const fetcher = (url) => axios.get(url).then((res) => res.data); + +export default fetcher; diff --git a/website/src/lib/poster.ts b/website/src/lib/poster.ts new file mode 100644 index 00000000..9d961806 --- /dev/null +++ b/website/src/lib/poster.ts @@ -0,0 +1,8 @@ +const poster = (url, { arg }) => { + return fetch(url, { + method: "POST", + body: JSON.stringify(arg), + }); +}; + +export default poster; diff --git a/website/lib/prismadb.ts b/website/src/lib/prismadb.ts similarity index 100% rename from website/lib/prismadb.ts rename to website/src/lib/prismadb.ts diff --git a/website/src/middleware.ts b/website/src/middleware.ts new file mode 100644 index 00000000..b383586b --- /dev/null +++ b/website/src/middleware.ts @@ -0,0 +1,8 @@ +export { default } from "next-auth/middleware"; + +/** + * Guards all pages under `/grading` and redirects them to the sign in page. + */ +export const config = { + matcher: ["/grading/:path*"], +}; diff --git a/website/src/pages/api/auth/[...nextauth].ts b/website/src/pages/api/auth/[...nextauth].ts new file mode 100644 index 00000000..1798dc90 --- /dev/null +++ b/website/src/pages/api/auth/[...nextauth].ts @@ -0,0 +1,38 @@ +import NextAuth from "next-auth"; +import DiscordProvider from "next-auth/providers/discord"; +import EmailProvider from "next-auth/providers/email"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; + +import prisma from "src/lib/prismadb"; + +export const authOptions = { + // Ensure we can store user data in a database. + adapter: PrismaAdapter(prisma), + providers: [ + // Register a Discord auth method. + DiscordProvider({ + clientId: process.env.DISCORD_CLIENT_ID, + clientSecret: process.env.DISCORD_CLIENT_SECRET, + }), + // Register an email magic link auth method. + EmailProvider({ + server: { + host: process.env.EMAIL_SERVER_HOST, + port: process.env.EMAIL_SERVER_PORT, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + }, + from: process.env.EMAIL_FROM, + }), + ], + pages: { + signIn: "/auth/signin", + }, + session: { + strategy: "jwt", + }, +}; + +export default NextAuth(authOptions); diff --git a/website/src/pages/api/new_task/[task_type].ts b/website/src/pages/api/new_task/[task_type].ts new file mode 100644 index 00000000..0d45c5f3 --- /dev/null +++ b/website/src/pages/api/new_task/[task_type].ts @@ -0,0 +1,71 @@ +import { getToken } from "next-auth/jwt"; + +import { authOptions } from "src/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 { task_type } = req.query; + + const token = await getToken({ req }); + + // Return nothing if the user isn't registered. + if (!token) { + 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: task_type, + user: { + id: token.sub, + display_name: token.name || token.email, + 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: token.sub, + }, + }, + }, + }); + + // 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/src/pages/api/update_task.ts b/website/src/pages/api/update_task.ts new file mode 100644 index 00000000..b7919c5a --- /dev/null +++ b/website/src/pages/api/update_task.ts @@ -0,0 +1,78 @@ +import { getToken } from "next-auth/jwt"; + +import { authOptions } from "src/pages/api/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 token = await getToken({ req }); + + // Return nothing if the user isn't registered. + if (!token) { + 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: token.sub, + display_name: token.name || token.email, + 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: token.sub, + }, + }, + }, + }); + + // 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/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx new file mode 100644 index 00000000..f15a2ab5 --- /dev/null +++ b/website/src/pages/auth/signin.tsx @@ -0,0 +1,85 @@ +import { getCsrfToken, getProviders, signIn } from "next-auth/react"; +import Head from "next/head"; +import Link from "next/link"; +import { useRef } from "react"; + +import { AuthLayout } from "src/components/AuthLayout"; + +export default function Signin({ csrfToken, providers }) { + const { discord, email } = providers; + const emailEl = useRef(null); + const signinWithEmail = () => { + signIn(email.id, { callbackUrl: "/", email: emailEl.current.value }); + }; + + return ( + <> + + Log in + + }> +
+ {discord && ( + + )} + + {email && ( +
+ + +
+ )} + + +
+
+ + ); +} + +export async function getServerSideProps(context) { + const csrfToken = await getCsrfToken(); + const providers = await getProviders(); + return { + props: { + csrfToken, + providers, + }, + }; +} diff --git a/website/src/pages/grading/grade-output.tsx b/website/src/pages/grading/grade-output.tsx index b063672b..792212f0 100644 --- a/website/src/pages/grading/grade-output.tsx +++ b/website/src/pages/grading/grade-output.tsx @@ -1,6 +1,63 @@ import { QuestionMarkCircleIcon } from "@heroicons/react/20/solid"; +import { useSession, signIn, signOut } from "next-auth/react"; +import { useEffect, useRef, useState } from "react"; +import useSWRImmutable from "swr/immutable"; +import useSWRMutation from "swr/mutation"; + +import fetcher from "src/lib/fetcher"; +import poster from "src/lib/poster"; + +const GradeOutput = () => { + // 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/rate_summary", fetcher, { + onSuccess: (data) => { + console.log(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", poster, { + 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: 2, + }, + }); + }; + + /** + * TODO: Make this a nicer loading screen. + */ + if (tasks.length == 0) { + return ( + <> +
+ + ); + } -export default function OutputDetail(): JSX.Element { return ( <>
@@ -11,7 +68,9 @@ export default function OutputDetail(): JSX.Element {
Instruction
-
{SAMPLE_PROMPT}
+
+ {tasks[0].task.full_text} +
@@ -19,7 +78,9 @@ export default function OutputDetail(): JSX.Element {
Output
-
{SAMPLE_OUTPUT}
+
+ {tasks[0].task.summary} +
{/* Form wrap*/}
@@ -66,13 +127,13 @@ export default function OutputDetail(): JSX.Element { Prompt - d1fb481a-e6cd-445d-9a15-8e2add854fe1 + {tasks[0].id}
Output - a5f85b0a-e11a-472c-bc73-946fdc2a6ec2 + {tasks.length === 2 ? tasks[1].id : "Submit your answer"}
@@ -86,6 +147,7 @@ export default function OutputDetail(): JSX.Element {
); -} +}; + +export default GradeOutput; function RatingButton(props: { rating: number; active: boolean }): JSX.Element { const activeClasses = @@ -183,20 +247,3 @@ const ANNOTATION_FLAGS: annotationBool[] = [ labelText: "Expresses moral judgement", }, ]; - -const SAMPLE_PROMPT = "Please make a list of aspects of a good pull request. Briefly describe each aspect."; - -const SAMPLE_OUTPUT = `Here are some aspects of a good pull request, which you may use to help your pull requests be good contributions and get accepted: - -1. Communicate: -2. Pull request size: - * Summary - * Fix one problem - * Limit the scope - * Use commits -3. Use coding conventions: -4. Perform testing: -5. Rebase onto the master branch before creating your PR -6. Respond to reviews quickly -7. Thank reviewers for their suggestions -`; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 2884cc2b..5c7b6acf 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -1,6 +1,7 @@ import { useSession } from "next-auth/react"; import Head from "next/head"; +import Link from "next/link"; import { CallToAction } from "../components/CallToAction"; import { Faq } from "../components/Faq"; @@ -35,14 +36,23 @@ export default function Home() { ); } return ( -
-
- {/* logo */} - + <> + + Open Assistant + + +
+

Open Chat Gpt

You are logged in

-
-
+ + ~Rate a prompt and output now~ + +