>
@@ -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 {
@@ -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 (
-
-
- {/* */}
-
+ <>
+
+ Open Assistant
+
+
+
+
Open Chat Gpt
You are logged in
-
-
+
+ ~Rate a prompt and output now~
+
+
+ >
);
}
diff --git a/website/src/pages/login.tsx b/website/src/pages/login.tsx
deleted file mode 100644
index 0f720af5..00000000
--- a/website/src/pages/login.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import Head from "next/head";
-import Link from "next/link";
-
-import { AuthLayout } from "../components/AuthLayout";
-
-export default function Login() {
- return (
- <>
-
- Log in
-
- >}>
-
-
- >
- );
-}
diff --git a/website/src/pages/new_task.js b/website/src/pages/new_task.tsx
similarity index 95%
rename from website/src/pages/new_task.js
rename to website/src/pages/new_task.tsx
index 123ee0a5..512381a1 100644
--- a/website/src/pages/new_task.js
+++ b/website/src/pages/new_task.tsx
@@ -1,4 +1,3 @@
-import axios from "axios";
import Head from "next/head";
import Image from "next/image";
import { useSession, signIn, signOut } from "next-auth/react";
@@ -6,7 +5,7 @@ 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);
+import fetcher from "src/lib/fetcher";
/**
* A helper function to post updates to tasks.
@@ -53,7 +52,7 @@ export default function NewPage() {
trigger({
id: t.id,
content: {
- rating: responseEl.current.value,
+ rating: 2,
},
});
};