diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5684bad2..ced8fcdd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -76,8 +76,12 @@ repos:
rev: v2.7.1
hooks:
- id: prettier
- args: [--prose-wrap=always, --write]
- exclude: website/tailwind.config.js|website/.storybook/main.js|website/.eslintrc.json
+ args:
+ [
+ --prose-wrap=always,
+ --write,
+ --ignore-path=./website/.prettierignore,
+ ]
- repo: local
hooks:
diff --git a/website/.prettierignore b/website/.prettierignore
new file mode 100644
index 00000000..0be2a485
--- /dev/null
+++ b/website/.prettierignore
@@ -0,0 +1,3 @@
+.eslintrc.json
+tailwind.config.js
+.storybook/*
diff --git a/website/src/.prettierrc.json b/website/.prettierrc.json
similarity index 100%
rename from website/src/.prettierrc.json
rename to website/.prettierrc.json
diff --git a/website/README.md b/website/README.md
index a30f2754..09b40e49 100644
--- a/website/README.md
+++ b/website/README.md
@@ -2,8 +2,7 @@
## Purpose
-This provides a comprehensive webapp interface for LAION's Open Assistant
-project. Initially it will support:
+This provides a comprehensive webapp interface for LAION's Open Assistant project. Initially it will support:
1. User registration using either Discord or Email.
1. Adding responses to incomplete Open Assistant tasks.
@@ -11,8 +10,7 @@ project. Initially it will support:
1. Viewing an activity leaderboard.
1. Tracking community wide updates.
-This interface compliments the Discord bot and will give access to the same
-underlying tasks.
+This interface compliments the Discord bot and will give access to the same underlying tasks.
## Contributing
@@ -22,67 +20,54 @@ This website is built using:
1. [npm](https://www.npmjs.com/): The node package manager for building.
1. [React](https://reactjs.org/): The core frontend framework.
-1. [Next.js](https://nextjs.org/): A React scaffolding framework to streamline
- development.
-1. [Prisma](https://www.prisma.io/): An ORM to interact with a web specific
- [Postgres](https://www.postgresql.org/) database.
-1. [NextAuth.js](https://next-auth.js.org/): A user authentication framework to
- ensure we handle accounts with best practices.
-1. [TailwindCSS](https://tailwindcss.com/): A general purpose framework for
- styling any component.
-1. [Chakra-UI](https://chakra-ui.com/): A wide collection of pre-built UI
- components that generally look pretty good.
+1. [Next.js](https://nextjs.org/): A React scaffolding framework to streamline development.
+1. [Prisma](https://www.prisma.io/): An ORM to interact with a web specific [Postgres](https://www.postgresql.org/)
+ database.
+1. [NextAuth.js](https://next-auth.js.org/): A user authentication framework to ensure we handle accounts with best
+ practices.
+1. [TailwindCSS](https://tailwindcss.com/): A general purpose framework for styling any component.
+1. [Chakra-UI](https://chakra-ui.com/): A wide collection of pre-built UI components that generally look pretty good.
### Set up your environment
-To contribute to the website, make sure you have the following setup and
-installed:
+To contribute to the website, make sure you have the following setup and installed:
-1. [NVM](https://github.com/nvm-sh/nvm): The Node Version Manager makes it easy
- to ensure you have the right NodeJS version installed. Once installed, run
- `nvm use 16` to use Node 16.x. The website is known to be stable with NodeJS
+1. [NVM](https://github.com/nvm-sh/nvm): The Node Version Manager makes it easy to ensure you have the right NodeJS
+ version installed. Once installed, run `nvm use 16` to use Node 16.x. The website is known to be stable with NodeJS
version 16.x. This will install both Node and NPM.
-1. [Docker](https://www.docker.com/): We use docker to simplify running
- dependent services.
+1. [Docker](https://www.docker.com/): We use docker to simplify running dependent services.
### Getting everything up and running
If you're doing active development we suggest the following workflow:
1. In one tab, navigate to the project root.
-1. Run `docker compose up frontend-dev --build --attach-dependencies`. You can
- optionally include `-d` to detach and later track the logs if desired.
+1. Run `docker compose up frontend-dev --build --attach-dependencies`. You can optionally include `-d` to detach and
+ later track the logs if desired.
1. In another tab navigate to `${OPEN_ASSISTANT_ROOT/website`.
1. Run `npm ci`
-1. Run `npx prisma db push` (This is also needed when you restart the docker
- stack from scratch).
-1. Run `npm run dev`. Now the website is up and running locally at
- `http://localhost:3000`.
-1. To create an account, login via the user using email authentication and
- navigate to `http://localhost:1080`. Check the email listed and click the
- log in link. You're now logged in and authenticated.
+1. Run `npx prisma db push` (This is also needed when you restart the docker stack from scratch).
+1. Run `npm run dev`. Now the website is up and running locally at `http://localhost:3000`.
+1. To create an account, login via the user using email authentication and navigate to `http://localhost:1080`. Check
+ the email listed and click the log in link. You're now logged in and authenticated.
### Using debug user credentials
-You can use the debug credentials provider to log in without fancy emails or
-OAuth.
+You can use the debug credentials provider to log in without fancy emails or OAuth.
-1. This feature is automatically on in development mode, i.e. when you run
- `npm run dev`. In case you want to do the same with a production build (for
- example, the docker image), then run the website with environment variable
+1. This feature is automatically on in development mode, i.e. when you run `npm run dev`. In case you want to do the
+ same with a production build (for example, the docker image), then run the website with environment variable
`DEBUG_LOGIN=true`.
1. Use the `Login` button in the top right to go to the login page.
-1. You should see a section for debug credentials. Enter any username you wish,
- you will be logged in as that user.
+1. You should see a section for debug credentials. Enter any username you wish, you will be logged in as that user.
### Using Storybook
-To develop components using [Storybook](https://storybook.js.org/) run
-`npm run storybook`. Then navigate to in your browser to
-`http://localhost:6006`.
+To develop components using [Storybook](https://storybook.js.org/) run `npm run storybook`. Then navigate to in your
+browser to `http://localhost:6006`.
-To create a new story create a file named `[componentName].stories.js`. An
-example how such a story could look like, see `Header.stories.jsx`.
+To create a new story create a file named `[componentName].stories.js`. An example how such a story could look like, see
+`Header.stories.jsx`.
## Code Layout
@@ -90,12 +75,10 @@ example how such a story could look like, see `Header.stories.jsx`.
All react code is under `src/` with a few sub directories:
-1. `pages/`: All pages a user could navigate too and API URLs which are under
- `pages/api/`.
-1. `components/`: All re-usable React components. If something gets used twice
- we should create a component and put it here.
-1. `lib/`: A generic place to store library files that are used anywhere. This
- doesn't have much structure yet.
+1. `pages/`: All pages a user could navigate too and API URLs which are under `pages/api/`.
+1. `components/`: All re-usable React components. If something gets used twice we should create a component and put it
+ here.
+1. `lib/`: A generic place to store library files that are used anywhere. This doesn't have much structure yet.
NOTE: `styles/` can be ignored for now.
@@ -113,25 +96,20 @@ We're not really using CSS styles. `styles/` can be ignored.
## Testing the UI
-Cypress is used for end-to-end (e2e) and component testing and is configured in
-`./cypress.config.ts`. The `./cypress` folder is used for supporting
-configuration files etc.
+Cypress is used for end-to-end (e2e) and component testing and is configured in `./cypress.config.ts`. The `./cypress`
+folder is used for supporting configuration files etc.
- Store e2e tests in the `./cypress/e2e` folder.
-- Store component tests adjacent to the component being tested. If you want to
- wriite a test for `./src/components/Layout.tsx` then store the test file at
- `./src/components/Layout.cy.tsx`.
+- Store component tests adjacent to the component being tested. If you want to wriite a test for
+ `./src/components/Layout.tsx` then store the test file at `./src/components/Layout.cy.tsx`.
A few npm scripts are available for convenience:
-- `npm run cypress`: Useful for development, it opens Cypress and allows you to
- explore, run and debug tests. It assumes you have the NextJS site running at
- `localhost:3000`.
-- `npm run cypress:run`: Runs all tests. Useful for a quick sanity check before
- sending a PR or to run in CI pipelines.
-- `npm run cypress:image-baseline`: If you have tests failing because of visual
- changes that was expected, this command will update the baseline images stored
- in `./cypress-visual-screenshots/baseline` with those from the adjacent
+- `npm run cypress`: Useful for development, it opens Cypress and allows you to explore, run and debug tests. It assumes
+ you have the NextJS site running at `localhost:3000`.
+- `npm run cypress:run`: Runs all tests. Useful for a quick sanity check before sending a PR or to run in CI pipelines.
+- `npm run cypress:image-baseline`: If you have tests failing because of visual changes that was expected, this command
+ will update the baseline images stored in `./cypress-visual-screenshots/baseline` with those from the adjacent
comparison folder. More can be found in the
[docs of `uktrade/cypress-image-diff`](https://github.com/uktrade/cypress-image-diff/blob/main/docs/CLI.md#update-all-baseline-images-for-failing-tests).
@@ -141,10 +119,9 @@ Read more in the [./cypress README](cypress/).
Jest and React Testing Library are used for unit testing JS/TS/TSX code.
-- Store unit test files adjacent to the file being tested and have the filename
- end with `.test.ts` for non-React code or `.test.tsx` for React code.
-- `npm run jest`: automatically runs tests and watches for any relevant changes
- to rerun tests.
+- Store unit test files adjacent to the file being tested and have the filename end with `.test.ts` for non-React code
+ or `.test.tsx` for React code.
+- `npm run jest`: automatically runs tests and watches for any relevant changes to rerun tests.
Read more in the [./src/README.md](src/README.md).
@@ -152,30 +129,25 @@ Read more in the [./src/README.md](src/README.md).
When writing code for the website, we have a few best practices:
-1. When importing packages import external dependencies first then local
- dependencies. Order them alphabetically according to the package name.
-1. When trying to implement something new, check if
- [Chakra-UI](https://chakra-ui.com/) has components that are close enough to
- your need. For example Sliders, Radio Buttons, Progress indicators, etc.
- They have a lot and we can save time by re-using what they have and tweaking
- the style as needed.
-1. Format everything with [Prettier](https://prettier.io/). This is done by
- default with pre-submits. We currently don't have any custom settings.
-1. Define functional React components (with types for all properties when
- feasible).
+1. When importing packages import external dependencies first then local dependencies. Order them alphabetically
+ according to the package name.
+1. When trying to implement something new, check if [Chakra-UI](https://chakra-ui.com/) has components that are close
+ enough to your need. For example Sliders, Radio Buttons, Progress indicators, etc. They have a lot and we can save
+ time by re-using what they have and tweaking the style as needed.
+1. Format everything with [Prettier](https://prettier.io/). This is done by default with pre-submits. We currently
+ don't have any custom settings.
+1. Define functional React components (with types for all properties when feasible).
### Developing New Features
-When working on new features or making significant changes that can't be done
-within a single Pull Request, we ask that you make use of Feature Flags.
+When working on new features or making significant changes that can't be done within a single Pull Request, we ask that
+you make use of Feature Flags.
-We've set up
-[`react-feature-flags`](https://www.npmjs.com/package/react-feature-flags) to
-make this easier. To get started:
+We've set up [`react-feature-flags`](https://www.npmjs.com/package/react-feature-flags) to make this easier. To get
+started:
-1. Add a new flag entry to `website/src/flags.ts`. We have an example flag you
- can copy as an example. Be sure to `isActive` to true when testing your
- features but false when submitting your PR.
+1. Add a new flag entry to `website/src/flags.ts`. We have an example flag you can copy as an example. Be sure to
+ `isActive` to true when testing your features but false when submitting your PR.
1. Use your flag wherever you add a new UI element. This can be done with:
```js
@@ -188,29 +160,24 @@ import { Flags } from "react-feature-flags";
You can see an example of how this works by checking `website/src/components/Header/Headers.tsx` where we use `flagTest`.
-1. Once you've finished building out the feature and it is ready for everyone
- to use, it's safe to remove the `Flag` wrappers around your component and
- the entry in `flags.ts`.
+1. Once you've finished building out the feature and it is ready for everyone to use, it's safe to remove the `Flag`
+ wrappers around your component and the entry in `flags.ts`.
### URL Paths
-To use stable and consistent URL paths, we recommend the following strategy for
-new tasks:
+To use stable and consistent URL paths, we recommend the following strategy for new tasks:
-1. For any task that involves writing a free-form response, put the page under
- `website/src/pages/create` with a page name matching the task type, such as
- `initial_prompt.tsx`.
-1. For any task that evaluates, rates, or ranks content, put the page under
- `website/src/pages/evaluate` with a page name matching the task type such as
- `rank_initial_prompts.tsx`.
+1. For any task that involves writing a free-form response, put the page under `website/src/pages/create` with a page
+ name matching the task type, such as `initial_prompt.tsx`.
+1. For any task that evaluates, rates, or ranks content, put the page under `website/src/pages/evaluate` with a page
+ name matching the task type such as `rank_initial_prompts.tsx`.
-With this we'll be able to ensure these contribution pages are hidden from
-logged out users but accessible to logged in users.
+With this we'll be able to ensure these contribution pages are hidden from logged out users but accessible to logged in
+users.
## Learn More
To learn more about Next.js, take a look at the following resources:
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
- features and API.
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
diff --git a/website/cypress/README.md b/website/cypress/README.md
index 4750cbf6..d6a2b383 100644
--- a/website/cypress/README.md
+++ b/website/cypress/README.md
@@ -1,24 +1,19 @@
# Component and e2e testing with Cypress
-[Cypress](https://www.cypress.io/) is used for both component- and end-to-end
-testing. Below there's a few examples for the context of this site. To learn
-more, the
-[Cypress documentation](https://docs.cypress.io/guides/getting-started/opening-the-app)
-has it all.
+[Cypress](https://www.cypress.io/) is used for both component- and end-to-end testing. Below there's a few examples for
+the context of this site. To learn more, the
+[Cypress documentation](https://docs.cypress.io/guides/getting-started/opening-the-app) has it all.
-Don't get scared by the commercial offerings they offer. Their core is open
-source, the cloud offering is not necesarry at all and can be replaced by CI
-tooling and [community efforts](https://sorry-cypress.dev/).
+Don't get scared by the commercial offerings they offer. Their core is open source, the cloud offering is not necesarry
+at all and can be replaced by CI tooling and [community efforts](https://sorry-cypress.dev/).
# Component testing
-To write a new component test, you either create a new `.tsx` adjacent to the
-component you want to test or you can use the guide presented yo you when
-running `npm run cypress` which allows you to easily create the skeleton test
-for an existing component.
+To write a new component test, you either create a new `.tsx` adjacent to the component you want to test or you can use
+the guide presented yo you when running `npm run cypress` which allows you to easily create the skeleton test for an
+existing component.
-If you have a `Button.tsx` component, create a file next to it called
-`Button.cy.tsx` which could look like this:
+If you have a `Button.tsx` component, create a file next to it called `Button.cy.tsx` which could look like this:
```typescript
import React from "react";
@@ -35,28 +30,24 @@ describe("", () => {
## What's happening here?
-First we use `cy.mount` to mount our component under test. Notive how we specify
-`className` and inner text - this is where we arrange our component with fake
-data that we could assert on later.
+First we use `cy.mount` to mount our component under test. Notive how we specify `className` and inner text - this is
+where we arrange our component with fake data that we could assert on later.
-In the example above, we also use `cy.get` to select the rendered `button`
-element. Cypress has multiple ways to
-[select elements](https://docs.cypress.io/guides/references/best-practices),
-`get` is just one of them (and often not recommended).
+In the example above, we also use `cy.get` to select the rendered `button` element. Cypress has multiple ways to
+[select elements](https://docs.cypress.io/guides/references/best-practices), `get` is just one of them (and often not
+recommended).
-At last, we use `captureSnapshot` which is a plugin that snaps a photo of the
-`button` element and compares it to a baseline located in the
-`./cypress-visual-screenshots/baseline/` folder. If there's too many unidentical
-pixels between the two, it will fail the test.
+At last, we use `captureSnapshot` which is a plugin that snaps a photo of the `button` element and compares it to a
+baseline located in the `./cypress-visual-screenshots/baseline/` folder. If there's too many unidentical pixels between
+the two, it will fail the test.
# End-to-end (e2e) testing
-e2e tests are stored in the `./cypress/e2e` folder and should be named
-`{page}.cy.ts` and located in a relative folder structure that mirrors the page
-under test.
+e2e tests are stored in the `./cypress/e2e` folder and should be named `{page}.cy.ts` and located in a relative folder
+structure that mirrors the page under test.
-When running `npm run cypress` and selecting e2e testing, we assume you have the
-NextJS site running at `localhost:3000`.
+When running `npm run cypress` and selecting e2e testing, we assume you have the NextJS site running at
+`localhost:3000`.
An example test could look as follows:
@@ -74,39 +65,33 @@ export {};
## What's happening here?
-First we use [`cy.visit`](https://docs.cypress.io/api/commands/visit) to point
-the browser at the desired page. It appends relative paths to the configured
-`baseUrl` (found in `./cypress.config.ts`).
+First we use [`cy.visit`](https://docs.cypress.io/api/commands/visit) to point the browser at the desired page. It
+appends relative paths to the configured `baseUrl` (found in `./cypress.config.ts`).
-Cypress will
-[automatically await](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Timeouts)
-almost anything you do, but fail if the default timeout is reached.
+Cypress will [automatically await](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Timeouts) almost
+anything you do, but fail if the default timeout is reached.
-Then we get the email input field and type our email address. We find the input
-field using the data-cy attribute that we added in the source code of the
-element on the page.
+Then we get the email input field and type our email address. We find the input field using the data-cy attribute that
+we added in the source code of the element on the page.
```jsx
```
-Using `data-cy` is how we ensure that selecting the element is robust to changes
-in page design or function and is one of the
+Using `data-cy` is how we ensure that selecting the element is robust to changes in page design or function and is one
+of the
[best practices recommended by Cypress](https://docs.cypress.io/guides/references/best-practices#Selecting-Elements).
-Next we call `type()` to use the keyboard, cypress will automatically focus the
-element and send the keypress events. Notice the `{enter}` keyword, this will
-cause Cypress to hit the return key which we expect to submit the form.
+Next we call `type()` to use the keyboard, cypress will automatically focus the element and send the keypress events.
+Notice the `{enter}` keyword, this will cause Cypress to hit the return key which we expect to submit the form.
-We then assert that the URL should contain `/auth/verify`. Again the timeout
-will make sure we are not waiting forever, and the test will fail if we do not
-manage to get there in a reasonable time.
+We then assert that the URL should contain `/auth/verify`. Again the timeout will make sure we are not waiting forever,
+and the test will fail if we do not manage to get there in a reasonable time.
## Authenticating in e2e tests
-For end-to-end tests almost every test will need to first sign in to the
-website. To make this easier we have a custom command for Cypress that makes
-logging in with an email address a single command, `cy.signInWithEmail()`.
+For end-to-end tests almost every test will need to first sign in to the website. To make this easier we have a custom
+command for Cypress that makes logging in with an email address a single command, `cy.signInWithEmail()`.
```typescript
describe("replying as the assistant", () => {
@@ -115,16 +100,13 @@ describe("replying as the assistant", () => {
cy.visit("/create/assistant_reply");
- cy.get('[data-cy="reply"').type(
- "You need to run pre-commit to make the reviewer happy."
- );
+ cy.get('[data-cy="reply"').type("You need to run pre-commit to make the reviewer happy.");
cy.get('[data-cy="submit"]').click();
});
});
```
-In this example we sign in as `cypress@example.com` before visiting the
-`/create/assistant_reply` page that is only available when authenticated. We can
-then continue on with our test as normal. Note: using `cy.signInWithEmail()`
-requires that the maildev is running, which should have been started as part of
-the `docker compose up` command that is required to do any end-to-end testing.
+In this example we sign in as `cypress@example.com` before visiting the `/create/assistant_reply` page that is only
+available when authenticated. We can then continue on with our test as normal. Note: using `cy.signInWithEmail()`
+requires that the maildev is running, which should have been started as part of the `docker compose up` command that is
+required to do any end-to-end testing.
diff --git a/website/cypress/components/Container.cy.tsx b/website/cypress/components/Container.cy.tsx
index 1e51fb06..0e085b6f 100644
--- a/website/cypress/components/Container.cy.tsx
+++ b/website/cypress/components/Container.cy.tsx
@@ -7,9 +7,6 @@ describe("", () => {
const className = "my-class";
const text = "test_container";
cy.mount({text});
- cy.get(`div.${className}`)
- .should("have.class", className)
- .should("be.visible")
- .should("contain", text);
+ cy.get(`div.${className}`).should("have.class", className).should("be.visible").should("contain", text);
});
});
diff --git a/website/cypress/contract/oasst_api_contract_tests.cy.ts b/website/cypress/contract/oasst_api_contract_tests.cy.ts
index d2ffeba3..82edeaaa 100644
--- a/website/cypress/contract/oasst_api_contract_tests.cy.ts
+++ b/website/cypress/contract/oasst_api_contract_tests.cy.ts
@@ -12,25 +12,18 @@ describe("Contract test for Oasst API", function () {
} as BackendUserCore;
it("can fetch a task", async () => {
- expect(await oasstApiClient.fetchTask("random", testUser)).to.be.not.null;
+ expect(await oasstApiClient.fetchTask("random", testUser, "en")).to.be.not.null;
});
it("can ack a task", async () => {
- const task = await oasstApiClient.fetchTask("random", testUser);
+ const task = await oasstApiClient.fetchTask("random", testUser, "en");
expect(await oasstApiClient.ackTask(task.id, "321")).to.be.null;
});
it("can record a taskInteraction", async () => {
- const task = await oasstApiClient.fetchTask("random", testUser);
+ const task = await oasstApiClient.fetchTask("random", testUser, "en");
expect(
- await oasstApiClient.interactTask(
- "text_reply_to_message",
- task.id,
- "321",
- "1",
- { text: "Test" },
- testUser
- )
+ await oasstApiClient.interactTask("text_reply_to_message", task.id, "321", "1", { text: "Test" }, testUser, "en")
).to.be.not.null;
});
diff --git a/website/cypress/e2e/tasks/random.cy.ts b/website/cypress/e2e/tasks/random.cy.ts
index 7e14dd1d..47a5b257 100644
--- a/website/cypress/e2e/tasks/random.cy.ts
+++ b/website/cypress/e2e/tasks/random.cy.ts
@@ -56,9 +56,7 @@ describe("handles random tasks", () => {
break;
}
case undefined: {
- throw new Error(
- "No tasks available, but at least create initial prompt expected"
- );
+ throw new Error("No tasks available, but at least create initial prompt expected");
}
default:
throw new Error(`Unexpected task type: ${type}`);
diff --git a/website/cypress/support/commands.ts b/website/cypress/support/commands.ts
index 096720d0..dd78aae5 100644
--- a/website/cypress/support/commands.ts
+++ b/website/cypress/support/commands.ts
@@ -37,19 +37,14 @@
// }
Cypress.Commands.add("signInUsingEmailedLink", (emailAddress) => {
- const mailDevApi = `${Cypress.env("MAILDEV_PROTOCOL")}://${Cypress.env(
- "MAILDEV_HOST"
- )}:${Cypress.env("MAILDEV_API_PORT")}`;
- cy.request(
- "GET",
- `${mailDevApi}/email?headers.to=${emailAddress.toLowerCase()}`
- ).then((response) => {
+ const mailDevApi = `${Cypress.env("MAILDEV_PROTOCOL")}://${Cypress.env("MAILDEV_HOST")}:${Cypress.env(
+ "MAILDEV_API_PORT"
+ )}`;
+ cy.request("GET", `${mailDevApi}/email?headers.to=${emailAddress.toLowerCase()}`).then((response) => {
const emails = response.body;
// Find and use login link
- const loginLink = emails
- .pop()
- .html.match(/href="[^"]+(\/api\/auth\/callback\/[^"]+?)"/)[1];
+ const loginLink = emails.pop().html.match(/href="[^"]+(\/api\/auth\/callback\/[^"]+?)"/)[1];
cy.visit(loginLink);
});
});
diff --git a/website/src/.prettierignore b/website/src/.prettierignore
deleted file mode 100644
index e69de29b..00000000
diff --git a/website/src/components/UserTable.tsx b/website/src/components/UserTable.tsx
index 71d4fe44..57a96c95 100644
--- a/website/src/components/UserTable.tsx
+++ b/website/src/components/UserTable.tsx
@@ -4,8 +4,7 @@ import { Pencil } from "lucide-react";
import Link from "next/link";
import { memo, useState } from "react";
import { get } from "src/lib/api";
-import { FetchUsersResponse } from "src/lib/oasst_api_client";
-import type { User } from "src/types/Users";
+import type { FetchUsersResponse, User } from "src/types/Users";
import useSWR from "swr";
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable";
diff --git a/website/src/lib/api.ts b/website/src/lib/api.ts
index df4bd399..d61016d2 100644
--- a/website/src/lib/api.ts
+++ b/website/src/lib/api.ts
@@ -17,7 +17,7 @@ export const post = (url: string, { arg: data }) => api.post(url, data).then((re
api.interceptors.response.use(
(response) => response,
(error) => {
- throw new OasstError(error.message ?? error, error.error_code);
+ throw new OasstError(error.message ?? error, error.error_code, error?.response?.status || -1);
}
);
diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts
index 7c48a288..bd263400 100644
--- a/website/src/lib/oasst_api_client.ts
+++ b/website/src/lib/oasst_api_client.ts
@@ -1,36 +1,20 @@
import type { Message } from "src/types/Conversation";
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
import type { AvailableTasks } from "src/types/Task";
-import type { BackendUser, BackendUserCore, User } from "src/types/Users";
+import type { BackendUser, BackendUserCore, FetchUsersParams, FetchUsersResponse } from "src/types/Users";
export class OasstError {
message: string;
errorCode: number;
httpStatusCode: number;
- constructor(message: string, errorCode: number, httpStatusCode?: number) {
+ constructor(message: string, errorCode: number, httpStatusCode: number) {
this.message = message;
this.errorCode = errorCode;
this.httpStatusCode = httpStatusCode;
}
}
-export type FetchUsersParams = {
- limit: number;
- cursor?: string;
- direction: "forward" | "back";
- searchDisplayName?: string;
- sortKey?: "username" | "display_name";
-};
-
-export type FetchUsersResponse = {
- items: T[];
- next?: string;
- prev?: string;
- sort_key: "username" | "display_name";
- order: "asc" | "desc";
-};
-
export class OasstApiClient {
oasstApiUrl: string;
oasstApiKey: string;
@@ -39,88 +23,6 @@ export class OasstApiClient {
this.oasstApiUrl = oasstApiUrl;
this.oasstApiKey = oasstApiKey;
}
-
- private async post(path: string, body: any): Promise {
- const resp = await fetch(`${this.oasstApiUrl}${path}`, {
- method: "POST",
- headers: {
- "X-API-Key": this.oasstApiKey,
- "Content-Type": "application/json",
- },
- body: JSON.stringify(body),
- });
-
- if (resp.status === 204) {
- return null;
- }
-
- if (resp.status >= 300) {
- const errorText = await resp.text();
- let error: any;
- try {
- error = JSON.parse(errorText);
- } catch (e) {
- throw new OasstError(errorText, 0, resp.status);
- }
- throw new OasstError(error.message ?? error, error.error_code, resp.status);
- }
-
- return await resp.json();
- }
-
- private async put(path: string): Promise {
- const resp = await fetch(`${this.oasstApiUrl}${path}`, {
- method: "PUT",
- headers: {
- "X-API-Key": this.oasstApiKey,
- },
- });
-
- if (resp.status === 204) {
- return null;
- }
-
- if (resp.status >= 300) {
- const errorText = await resp.text();
- let error: any;
- try {
- error = JSON.parse(errorText);
- } catch (e) {
- throw new OasstError(errorText, 0, resp.status);
- }
- throw new OasstError(error.message ?? error, error.error_code, resp.status);
- }
-
- return await resp.json();
- }
-
- private async get(path: string): Promise {
- const resp = await fetch(`${this.oasstApiUrl}${path}`, {
- method: "GET",
- headers: {
- "X-API-Key": this.oasstApiKey,
- "Content-Type": "application/json",
- },
- });
-
- if (resp.status === 204) {
- return null;
- }
-
- if (resp.status >= 300) {
- const errorText = await resp.text();
- let error: any;
- try {
- error = JSON.parse(errorText);
- } catch (e) {
- throw new OasstError(errorText, 0, resp.status);
- }
- throw new OasstError(error.message ?? error, error.error_code, resp.status);
- }
-
- return await resp.json();
- }
-
// TODO return a strongly typed Task?
// This method is used to store a task in RegisteredTask.task.
// This is a raw Json type, so we can't use it to strongly type the task.
@@ -132,13 +34,13 @@ export class OasstApiClient {
});
}
- async ackTask(taskId: string, messageId: string): Promise {
+ async ackTask(taskId: string, messageId: string): Promise {
return this.post(`/api/v1/tasks/${taskId}/ack`, {
message_id: messageId,
});
}
- async nackTask(taskId: string, reason: string): Promise {
+ async nackTask(taskId: string, reason: string): Promise {
return this.post(`/api/v1/tasks/${taskId}/nack`, {
reason,
});
@@ -170,8 +72,8 @@ export class OasstApiClient {
/**
* Returns the tasks availability information for given `user`.
*/
- async fetch_tasks_availability(user: object): Promise {
- return this.post("/api/v1/tasks/availability", user);
+ async fetch_tasks_availability(user: object): Promise {
+ return this.post("/api/v1/tasks/availability", user);
}
/**
@@ -191,18 +93,12 @@ export class OasstApiClient {
/**
* Returns the `BackendUser` associated with `user_id`
*/
- async fetch_user(user_id: string): Promise {
+ async fetch_user(user_id: string): Promise {
return this.get(`/api/v1/users/${user_id}`);
}
/**
* Returns the set of `BackendUser`s stored by the backend.
- *
- * @param {number} max_count - The maximum number of users to fetch.
- * @param {string} cursor - The user's `display_name` to use when paginating.
- * @param {boolean} isForward - If true and `cursor` is not empty, pages
- * forward. If false and `cursor` is not empty, pages backwards.
- * @returns {Promise} A Promise that returns an array of `BackendUser` objects.
*/
async fetch_users({
direction,
@@ -210,46 +106,28 @@ export class OasstApiClient {
cursor,
searchDisplayName,
sortKey = "display_name",
- }: FetchUsersParams): Promise {
- const params = new URLSearchParams({
+ }: FetchUsersParams): Promise {
+ return this.get(`/api/v1/users/cursor`, {
search_text: searchDisplayName,
sort_key: sortKey,
- max_count: limit.toString(),
+ max_count: limit,
+ after: direction === "forward" ? cursor : undefined,
+ before: direction === "back" ? cursor : undefined,
});
-
- // The backend API uses different query parameters depending on the
- // pagination direction but they both take the same cursor value.
- // Depending on direction, pick the right query param.
- if (cursor !== "") {
- params.append(direction === "forward" ? "after" : "before", cursor);
- }
- const BASE_URL = `/api/v1/users/cursor`;
- const url = `${BASE_URL}/?${params.toString()}`;
- return this.get(url);
}
- // async fetch_user_by_display_name(name: string): Promise {
- // const params = new URLSearchParams({
- // search_text: name,
- // });
-
- // const endpoint = `/api/v1/frontend_users/by_display_name`;
-
- // return this.get(`${endpoint}?${params.toString()}`);
- // }
-
/**
* Returns the `Message`s associated with `user_id` in the backend.
*/
- async fetch_user_messages(user_id: string): Promise {
- return this.get(`/api/v1/users/${user_id}/messages`);
+ async fetch_user_messages(user_id: string): Promise {
+ return this.get(`/api/v1/users/${user_id}/messages`);
}
/**
* Updates the backend's knowledge about the `user_id`.
*/
- async set_user_status(user_id: string, is_enabled: boolean, notes): Promise {
- return this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}¬es=${notes}`);
+ async set_user_status(user_id: string, is_enabled: boolean, notes: string): Promise {
+ await this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}¬es=${notes}`);
}
/**
@@ -265,18 +143,69 @@ export class OasstApiClient {
async fetch_leaderboard(
time_frame: LeaderboardTimeFrame,
{ limit = 20 }: { limit?: number }
- ): Promise {
- const params = new URLSearchParams({
- limit: limit.toString(),
- });
- return this.get(`/api/v1/leaderboards/${time_frame}?${params.toString()}`);
+ ): Promise {
+ return this.get(`/api/v1/leaderboards/${time_frame}`, { limit });
}
/**
* Returns the counts of all tasks (some might be zero)
*/
- async fetch_available_tasks(user: BackendUserCore, lang: string): Promise {
- return this.post(`/api/v1/tasks/availability?lang=${lang}`, user);
+ async fetch_available_tasks(user: BackendUserCore, lang: string): Promise {
+ return this.post(`/api/v1/tasks/availability?lang=${lang}`, user);
+ }
+
+ private async post(path: string, body: unknown) {
+ return this.request("POST", path, {
+ body: JSON.stringify(body),
+ });
+ }
+
+ private async put(path: string) {
+ return this.request("PUT", path);
+ }
+
+ private async get(path: string, query?: Record) {
+ if (!query) {
+ return this.request("GET", path);
+ }
+
+ const filteredQuery = Object.fromEntries(
+ Object.entries(query).filter(([, value]) => value !== undefined)
+ ) as Record;
+
+ const params = new URLSearchParams(filteredQuery).toString();
+
+ return this.request("GET", `${path}?${params}`);
+ }
+
+ private async request(method: "GET" | "POST" | "PUT", path: string, init?: RequestInit): Promise {
+ const resp = await fetch(`${this.oasstApiUrl}${path}`, {
+ method,
+ ...init,
+ headers: {
+ "X-API-Key": this.oasstApiKey,
+ "Content-Type": "application/json",
+ ...init?.headers,
+ },
+ });
+
+ if (resp.status === 204) {
+ return null;
+ }
+
+ if (resp.status >= 300) {
+ const errorText = await resp.text();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let error: any;
+ try {
+ error = JSON.parse(errorText);
+ } catch (e) {
+ throw new OasstError(errorText, 0, resp.status);
+ }
+ throw new OasstError(error.message ?? error, error.error_code, resp.status);
+ }
+
+ return await resp.json();
}
}
diff --git a/website/src/pages/api/admin/users.ts b/website/src/pages/api/admin/users.ts
index 57944cff..d10c91b0 100644
--- a/website/src/pages/api/admin/users.ts
+++ b/website/src/pages/api/admin/users.ts
@@ -1,6 +1,7 @@
import { withRole } from "src/lib/auth";
-import { FetchUsersParams, oasstApiClient } from "src/lib/oasst_api_client";
+import { oasstApiClient } from "src/lib/oasst_api_client";
import prisma from "src/lib/prismadb";
+import { FetchUsersParams } from "src/types/Users";
/**
* The number of users to fetch in a single request. Could later be a query parameter.
diff --git a/website/src/types/Users.ts b/website/src/types/Users.ts
index 39d2a663..52240b5f 100644
--- a/website/src/types/Users.ts
+++ b/website/src/types/Users.ts
@@ -51,3 +51,19 @@ export interface User extends BackendUser {
*/
role: string;
}
+
+export type FetchUsersParams = {
+ limit: number;
+ cursor?: string;
+ direction: "forward" | "back";
+ searchDisplayName?: string;
+ sortKey?: "username" | "display_name";
+};
+
+export type FetchUsersResponse = {
+ items: T[];
+ next?: string;
+ prev?: string;
+ sort_key: "username" | "display_name";
+ order: "asc" | "desc";
+};
diff --git a/website/styles/Home.module.css b/website/styles/Home.module.css
index 69ab0c28..1dc2ec1c 100644
--- a/website/styles/Home.module.css
+++ b/website/styles/Home.module.css
@@ -1,45 +1,41 @@
.App {
- text-align: center;
+ text-align: center;
}
.App-logo {
- height: 40vmin;
- pointer-events: none;
+ height: 40vmin;
+ pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
}
.AppHeader {
- background: linear-gradient(
- 217deg,
- rgba(255, 0, 0, 0.8),
- rgba(255, 0, 0, 0) 70.71%
- ),
- linear-gradient(127deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 70.71%),
- linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%);
- background: black;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
+ background: linear-gradient(217deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 70.71%),
+ linear-gradient(127deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 70.71%),
+ linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%);
+ background: black;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
}
.AppLink {
- color: #61dafb;
+ color: #61dafb;
}
@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
diff --git a/website/styles/Theme/index.tsx b/website/styles/Theme/index.tsx
index 9e9b0d82..7288b7ec 100644
--- a/website/styles/Theme/index.tsx
+++ b/website/styles/Theme/index.tsx
@@ -1,8 +1,4 @@
-import {
- type ThemeConfig,
- extendTheme,
- usePrefersReducedMotion,
-} from "@chakra-ui/react";
+import { type ThemeConfig, extendTheme, usePrefersReducedMotion } from "@chakra-ui/react";
import { containerTheme } from "./Components/Container";
import { StyleFunctionProps, Styles } from "@chakra-ui/theme-tools";