mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-08-13 11:50:37 +08:00
Merging from main
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.eslintrc.json
|
||||
tailwind.config.js
|
||||
.storybook/*
|
||||
@@ -10,6 +10,7 @@ module.exports = {
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@chakra-ui/storybook-addon",
|
||||
"storybook-addon-next-router",
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
@@ -23,7 +24,12 @@ module.exports = {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
src: path.resolve(__dirname, "../src"),
|
||||
styles: path.resolve(__dirname, "../styles"),
|
||||
};
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
path: require.resolve('path-browserify'),
|
||||
}
|
||||
return config;
|
||||
},
|
||||
features: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "!style-loader!css-loader!postcss-loader!tailwindcss/tailwind.css";
|
||||
import { RouterContext } from "next/dist/shared/lib/router-context";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
@@ -8,6 +9,9 @@ export const parameters = {
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
},
|
||||
};
|
||||
|
||||
// Hacky solution to get Images in next to work
|
||||
|
||||
+67
-100
@@ -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.
|
||||
|
||||
+40
-58
@@ -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("<Button />", () => {
|
||||
|
||||
## 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
|
||||
<Input data-cy="email-address" placeholder="Email Address" />
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -7,9 +7,6 @@ describe("<Container />", () => {
|
||||
const className = "my-class";
|
||||
const text = "test_container";
|
||||
cy.mount(<Container className={className}>{text}</Container>);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
describe("labeling assistant replies", () => {
|
||||
it("completes the current task on submit and on request shows a new task", () => {
|
||||
cy.signInWithEmail("cypress@example.com");
|
||||
cy.visit("/label/label_assistant_reply");
|
||||
|
||||
cy.get('[data-cy="task"]')
|
||||
.invoke("attr", "data-task-type")
|
||||
.then((type) => {
|
||||
cy.log("Task type", type);
|
||||
|
||||
// For specific task pages the no task available result is normal.
|
||||
if (type === undefined) return;
|
||||
|
||||
cy.get('[data-cy="label-options"]').each((label) => {
|
||||
// Click the 4th option
|
||||
cy.wrap(label).find('[data-cy="radio-option"]').eq(3).click();
|
||||
});
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,26 @@
|
||||
describe("labeling initial prompts", () => {
|
||||
it("completes the current task on submit and on request shows a new task", () => {
|
||||
cy.signInWithEmail("cypress@example.com");
|
||||
cy.visit("/label/label_initial_prompt");
|
||||
|
||||
cy.get('[data-cy="task"]')
|
||||
.invoke("attr", "data-task-type")
|
||||
.then((type) => {
|
||||
cy.log("Task type", type);
|
||||
|
||||
// For specific task pages the no task available result is normal.
|
||||
if (type === undefined) return;
|
||||
|
||||
cy.get('[data-cy="label-options"]').each((label) => {
|
||||
// Click the 4th option
|
||||
cy.wrap(label).find('[data-cy="radio-option"]').eq(3).click();
|
||||
});
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,26 @@
|
||||
describe("labeling prompter replies", () => {
|
||||
it("completes the current task on submit and on request shows a new task", () => {
|
||||
cy.signInWithEmail("cypress@example.com");
|
||||
cy.visit("/label/label_prompter_reply");
|
||||
|
||||
cy.get('[data-cy="task"]')
|
||||
.invoke("attr", "data-task-type")
|
||||
.then((type) => {
|
||||
cy.log("Task type", type);
|
||||
|
||||
// For specific task pages the no task available result is normal.
|
||||
if (type === undefined) return;
|
||||
|
||||
cy.get('[data-cy="label-options"]').each((label) => {
|
||||
// Click the 4th option
|
||||
cy.wrap(label).find('[data-cy="radio-option"]').eq(3).click();
|
||||
});
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,23 @@
|
||||
describe("no tasks available", () => {
|
||||
it("displays an empty state when no tasks are available", () => {
|
||||
cy.signInWithEmail("cypress@example.com");
|
||||
cy.intercept(
|
||||
{
|
||||
method: "GET",
|
||||
url: "/api/new_task/prompter_reply",
|
||||
},
|
||||
{
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: "No tasks of type 'label_prompter_reply' are currently available.",
|
||||
errorCode: 1006,
|
||||
httpStatusCode: 503,
|
||||
},
|
||||
}
|
||||
).as("newTaskPrompterReply");
|
||||
cy.visit("/create/user_reply");
|
||||
cy.wait("@newTaskPrompterReply").then(() => {
|
||||
cy.get('[data-cy="cy-no-tasks"]').should("exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,47 +44,29 @@ describe("handles random tasks", () => {
|
||||
break;
|
||||
}
|
||||
case "label-task": {
|
||||
cy.get('[data-cy="label-group-item"]')
|
||||
.first()
|
||||
.invoke("attr", "data-label-type")
|
||||
.then((label_type) => {
|
||||
const parent = cy
|
||||
.get('[data-cy="label-group-item"]')
|
||||
.first();
|
||||
cy.log("Label type", label_type);
|
||||
cy.get('[data-cy="label-options"]').each((label) => {
|
||||
// Click the 4th option
|
||||
cy.wrap(label).find('[data-cy="radio-option"]').eq(3).click();
|
||||
});
|
||||
|
||||
switch (label_type) {
|
||||
case "slider": {
|
||||
// Clicking on the slider will set the value to about the middle where it clicks
|
||||
parent
|
||||
.get('[aria-roledescription="slider"]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
|
||||
break;
|
||||
}
|
||||
case "radio": {
|
||||
// Clicking on the slider will set the value to about the middle where it clicks
|
||||
parent
|
||||
.get('[aria-roledescription="radio-button"]')
|
||||
.last()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
|
||||
break;
|
||||
}
|
||||
case "spam-task": {
|
||||
cy.get('[data-cy="not-spam-button"]').click();
|
||||
|
||||
cy.get('[data-cy="review"]').click();
|
||||
|
||||
cy.get('[data-cy="submit"]').click();
|
||||
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
throw new Error("No tasks available, but at least create initial prompt expected");
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected task type: ${type}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
locales: ["bn", "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh"],
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+174
-149
@@ -21,6 +21,8 @@
|
||||
"@next/font": "^13.1.0",
|
||||
"@prisma/client": "^4.7.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-table": "^8.7.6",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"axios": "^1.2.1",
|
||||
"boolean": "^3.2.0",
|
||||
@@ -31,6 +33,7 @@
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^6.5.1",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.105.0",
|
||||
"next": "13.0.6",
|
||||
"next-auth": "^4.18.6",
|
||||
"next-i18next": "^13.0.3",
|
||||
@@ -38,13 +41,13 @@
|
||||
"npm": "^9.2.0",
|
||||
"postcss-focus-visible": "^7.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-cookies": "^0.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-feature-flags": "^1.0.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-i18next": "^12.1.4",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.3",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"swr": "^2.0.0",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"unique-username-generator": "^1.1.3",
|
||||
@@ -6199,7 +6202,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.15.tgz",
|
||||
"integrity": "sha512-cnLzVK1S+EydFDSuvxMmMAxVqNXijBGdV9QTgsu6ys5sOkoiXRETKZmxuN8/ZRbkfc4N+1KAylSCZOOHzBQTBQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/api": "6.5.15",
|
||||
@@ -6242,7 +6244,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz",
|
||||
"integrity": "sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"is-dom": "^1.0.0",
|
||||
@@ -6737,7 +6738,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz",
|
||||
"integrity": "sha512-xT31SuSX+kYGyxCNK2nqL7WTxucs3rSmhiCLovJcUjYk+QquV3c2c53Ki7lwwdDbzfXFcNAe0HJ4hoTN4jhn0Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/api": "6.5.15",
|
||||
"@storybook/channels": "6.5.15",
|
||||
@@ -6764,7 +6764,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.15.tgz",
|
||||
"integrity": "sha512-BBE0KXKvj1/3jTghbIoWfrcDM0t+xO7EYtWWAXD6XlhGsZVD2Dy82Z52ONyLulMDRpMWl0OYy3h6A1YnFUH25w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/channels": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -8234,7 +8233,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.15.tgz",
|
||||
"integrity": "sha512-gMpA8LWT8lC4z5KWnaMh03aazEwtDO7GtY5kZVru+EEMgExGmaR82qgekwmLmgLj2nRJEv0o138o9IqYUcou8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/channels": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -8270,7 +8268,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.15.tgz",
|
||||
"integrity": "sha512-gPpsBgirv2NCXbH4WbYqdkI0JLE96aiVuu7UEWfn9yu071pQ9CLHbhXGD9fSFNrfOkyBBY10ppSE7uCXw3Wexg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.2",
|
||||
"ts-dedent": "^2.0.0",
|
||||
@@ -8285,7 +8282,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.15.tgz",
|
||||
"integrity": "sha512-0ZGpRgVz7rdbCguBqBpwObXbsVY5qlSTWDzzIBpmz8EkxW/MtK5wEyeq+0L0O+DTn41FwvH5yCGLAENpzWD8BQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/channel-postmessage": "6.5.15",
|
||||
@@ -8321,7 +8317,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.15.tgz",
|
||||
"integrity": "sha512-0uyxKvodq+FycGv6aUwC1wUR6suXf2+7ywMFAOlYolI4UvNj8NyU/5AfgKT5XnxYAgPmoCiAjOE700TrfHrosw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.2",
|
||||
"global": "^4.4.0"
|
||||
@@ -8335,7 +8330,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.15.tgz",
|
||||
"integrity": "sha512-bHTT0Oa3s4g+MBMaLBbX9ofMtb1AW59AzIUNGrfqW1XqJMGuUHMiJ7TSo+i5dRSFpbFygnwMEG9LfHxpR2Z0Dw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"@storybook/csf": "0.0.2--canary.4566f4d.1",
|
||||
@@ -9234,7 +9228,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.15.tgz",
|
||||
"integrity": "sha512-B1Ba6l5W7MeNclclqMMTMHgYgfdpB5SIhNCQFnzIz8blynzRhNFMdxvbAl6Je5G0S4xydYYd7Lno2kXQebs7HA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.2"
|
||||
},
|
||||
@@ -10019,7 +10012,6 @@
|
||||
"version": "0.0.2--canary.4566f4d.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz",
|
||||
"integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
@@ -11967,7 +11959,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.15.tgz",
|
||||
"integrity": "sha512-9t8rI8t7/Krolau29gsdjdbRQ66orONIyP0efp0EukVgv6reNFzb/U14ARrl0uHys6Tl5Xyece9FoakQUdn8Kg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"core-js": "^3.8.2",
|
||||
@@ -11988,7 +11979,6 @@
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"find-up": "^4.1.0"
|
||||
@@ -12004,7 +11994,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -12017,7 +12006,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
@@ -12029,7 +12017,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
@@ -12044,7 +12031,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
@@ -12094,7 +12080,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.15.tgz",
|
||||
"integrity": "sha512-r6cYTf6GtbqgdI4ZG3xuWdJAAu5fJ3xAWMiDkHyoK2u+R2TeYXIsJvgn0BPrW87sZhELIkg4ckdFECmATs3kpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -12232,7 +12217,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.15.tgz",
|
||||
"integrity": "sha512-pgdW0lVZKKXQ4VhIfLHycMmwFSVOY7vLTKnytag4Y8Yz+aXm0bwDN/QxPntFzDH47F1Rcy2ywNnvty8ooDTvuA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"core-js": "^3.8.2",
|
||||
@@ -12297,6 +12281,37 @@
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.6.tgz",
|
||||
"integrity": "sha512-/QijmMFeP7wDLBnr0MQ/5MlbXePbIL/1nOtkxBC9zvmBu4gDKJEDBqipUyM7Wc/iBpSd0IFyqBlvZvTPD9FYDA==",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.7.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.6.tgz",
|
||||
"integrity": "sha512-sqiNTMzB6cpyL8DFH6/VqW48SwiflLqxQqYpo2wNock7rdVGvlm0BLNI8vZUJbr1+fmmWmHwBvi5OMgZw8n1DA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "8.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz",
|
||||
@@ -12674,8 +12689,7 @@
|
||||
"node_modules/@types/is-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.1.tgz",
|
||||
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q=="
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.4",
|
||||
@@ -12876,8 +12890,7 @@
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.0.26",
|
||||
@@ -12995,8 +13008,7 @@
|
||||
"node_modules/@types/webpack-env": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz",
|
||||
"integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg=="
|
||||
},
|
||||
"node_modules/@types/webpack-sources": {
|
||||
"version": "3.2.0",
|
||||
@@ -13616,6 +13628,11 @@
|
||||
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accept-language-parser": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
|
||||
"integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -17984,8 +18001,7 @@
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/domain-browser": {
|
||||
"version": "1.2.0",
|
||||
@@ -20786,7 +20802,6 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
@@ -22018,7 +22033,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-dom/-/is-dom-1.1.0.tgz",
|
||||
"integrity": "sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-object": "^1.0.1",
|
||||
"is-window": "^1.0.2"
|
||||
@@ -22069,8 +22083,7 @@
|
||||
"node_modules/is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"node_modules/is-generator-fn": {
|
||||
"version": "2.1.0",
|
||||
@@ -22164,7 +22177,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -22369,8 +22381,7 @@
|
||||
"node_modules/is-window": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
|
||||
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg=="
|
||||
},
|
||||
"node_modules/is-windows": {
|
||||
"version": "1.0.2",
|
||||
@@ -26446,8 +26457,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -26687,6 +26697,14 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.105.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.105.0.tgz",
|
||||
"integrity": "sha512-iHaIkd4Wq6aNIVrFMXt3If8E/+2lnJd4WlCyntoJNIzZ8nWhdSSHWpsw7XM4rlw2319LZ2t4WLdnM8Z0ECDTOQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
@@ -26776,8 +26794,7 @@
|
||||
"node_modules/map-or-similar": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz",
|
||||
"integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg=="
|
||||
},
|
||||
"node_modules/map-visit": {
|
||||
"version": "1.0.0",
|
||||
@@ -26924,7 +26941,6 @@
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
|
||||
"integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"map-or-similar": "^1.5.0"
|
||||
}
|
||||
@@ -27183,7 +27199,6 @@
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
@@ -31128,7 +31143,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -31480,7 +31494,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz",
|
||||
"integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8"
|
||||
},
|
||||
@@ -32087,7 +32100,6 @@
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -32269,7 +32281,6 @@
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
@@ -32466,6 +32477,23 @@
|
||||
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cookies": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-cookies/-/react-cookies-0.1.1.tgz",
|
||||
"integrity": "sha512-PP75kJ4vtoHuuTdq0TAD3RmlAv7vuDQh9fkC4oDlhntgs9vX1DmREomO0Y1mcQKR9nMZ6/zxoflaMJ3MAmF5KQ==",
|
||||
"dependencies": {
|
||||
"cookie": "^0.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cookies/node_modules/cookie": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
||||
"integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-docgen": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||
@@ -32601,14 +32629,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
|
||||
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -32700,18 +32720,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-table": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
|
||||
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -34576,8 +34584,7 @@
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility",
|
||||
"dev": true
|
||||
"deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility"
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
@@ -34730,8 +34737,31 @@
|
||||
"node_modules/store2": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz",
|
||||
"integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w=="
|
||||
},
|
||||
"node_modules/storybook-addon-next-router": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook-addon-next-router/-/storybook-addon-next-router-4.0.2.tgz",
|
||||
"integrity": "sha512-0rjGAl7HziW4ecDq+VU03H1dwkw5f6phqA+PMquPzdowNVl29ejVwVadLMGlovG6x2snaxMGxtySR2c5bwegSw==",
|
||||
"dependencies": {
|
||||
"tslib": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@storybook/addon-actions": "^6.0.0",
|
||||
"@storybook/addons": "^6.0.0",
|
||||
"@storybook/client-api": "^6.0.0",
|
||||
"next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/storybook-addon-next-router/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/stream-browserify": {
|
||||
"version": "2.0.2",
|
||||
@@ -35176,8 +35206,7 @@
|
||||
"node_modules/synchronous-promise": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.16.tgz",
|
||||
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A=="
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.8.4",
|
||||
@@ -35334,7 +35363,6 @@
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz",
|
||||
"integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/is-function": "^1.0.0",
|
||||
"global": "^4.4.0",
|
||||
@@ -35350,7 +35378,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -35721,7 +35748,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
|
||||
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.10"
|
||||
}
|
||||
@@ -36514,8 +36540,7 @@
|
||||
"node_modules/uuid-browser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz",
|
||||
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg=="
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
@@ -42022,7 +42047,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.5.15.tgz",
|
||||
"integrity": "sha512-cnLzVK1S+EydFDSuvxMmMAxVqNXijBGdV9QTgsu6ys5sOkoiXRETKZmxuN8/ZRbkfc4N+1KAylSCZOOHzBQTBQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/api": "6.5.15",
|
||||
@@ -42049,7 +42073,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz",
|
||||
"integrity": "sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"is-dom": "^1.0.0",
|
||||
@@ -42319,7 +42342,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.15.tgz",
|
||||
"integrity": "sha512-xT31SuSX+kYGyxCNK2nqL7WTxucs3rSmhiCLovJcUjYk+QquV3c2c53Ki7lwwdDbzfXFcNAe0HJ4hoTN4jhn0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/api": "6.5.15",
|
||||
"@storybook/channels": "6.5.15",
|
||||
@@ -42338,7 +42360,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.15.tgz",
|
||||
"integrity": "sha512-BBE0KXKvj1/3jTghbIoWfrcDM0t+xO7EYtWWAXD6XlhGsZVD2Dy82Z52ONyLulMDRpMWl0OYy3h6A1YnFUH25w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/channels": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -43496,7 +43517,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.15.tgz",
|
||||
"integrity": "sha512-gMpA8LWT8lC4z5KWnaMh03aazEwtDO7GtY5kZVru+EEMgExGmaR82qgekwmLmgLj2nRJEv0o138o9IqYUcou8w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/channels": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -43524,7 +43544,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.15.tgz",
|
||||
"integrity": "sha512-gPpsBgirv2NCXbH4WbYqdkI0JLE96aiVuu7UEWfn9yu071pQ9CLHbhXGD9fSFNrfOkyBBY10ppSE7uCXw3Wexg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^3.8.2",
|
||||
"ts-dedent": "^2.0.0",
|
||||
@@ -43535,7 +43554,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.5.15.tgz",
|
||||
"integrity": "sha512-0ZGpRgVz7rdbCguBqBpwObXbsVY5qlSTWDzzIBpmz8EkxW/MtK5wEyeq+0L0O+DTn41FwvH5yCGLAENpzWD8BQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/channel-postmessage": "6.5.15",
|
||||
@@ -43563,7 +43581,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.15.tgz",
|
||||
"integrity": "sha512-0uyxKvodq+FycGv6aUwC1wUR6suXf2+7ywMFAOlYolI4UvNj8NyU/5AfgKT5XnxYAgPmoCiAjOE700TrfHrosw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^3.8.2",
|
||||
"global": "^4.4.0"
|
||||
@@ -43573,7 +43590,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.15.tgz",
|
||||
"integrity": "sha512-bHTT0Oa3s4g+MBMaLBbX9ofMtb1AW59AzIUNGrfqW1XqJMGuUHMiJ7TSo+i5dRSFpbFygnwMEG9LfHxpR2Z0Dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"@storybook/csf": "0.0.2--canary.4566f4d.1",
|
||||
@@ -44298,7 +44314,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.15.tgz",
|
||||
"integrity": "sha512-B1Ba6l5W7MeNclclqMMTMHgYgfdpB5SIhNCQFnzIz8blynzRhNFMdxvbAl6Je5G0S4xydYYd7Lno2kXQebs7HA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^3.8.2"
|
||||
}
|
||||
@@ -44949,7 +44964,6 @@
|
||||
"version": "0.0.2--canary.4566f4d.1",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz",
|
||||
"integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
@@ -46441,7 +46455,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.15.tgz",
|
||||
"integrity": "sha512-9t8rI8t7/Krolau29gsdjdbRQ66orONIyP0efp0EukVgv6reNFzb/U14ARrl0uHys6Tl5Xyece9FoakQUdn8Kg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"core-js": "^3.8.2",
|
||||
@@ -46454,7 +46467,6 @@
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^3.6.5",
|
||||
"find-up": "^4.1.0"
|
||||
@@ -46464,7 +46476,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -46474,7 +46485,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
@@ -46483,7 +46493,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
@@ -46492,7 +46501,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
@@ -46529,7 +46537,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/store/-/store-6.5.15.tgz",
|
||||
"integrity": "sha512-r6cYTf6GtbqgdI4ZG3xuWdJAAu5fJ3xAWMiDkHyoK2u+R2TeYXIsJvgn0BPrW87sZhELIkg4ckdFECmATs3kpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/addons": "6.5.15",
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
@@ -46636,7 +46643,6 @@
|
||||
"version": "6.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.15.tgz",
|
||||
"integrity": "sha512-pgdW0lVZKKXQ4VhIfLHycMmwFSVOY7vLTKnytag4Y8Yz+aXm0bwDN/QxPntFzDH47F1Rcy2ywNnvty8ooDTvuA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@storybook/client-logger": "6.5.15",
|
||||
"core-js": "^3.8.2",
|
||||
@@ -46682,6 +46688,19 @@
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
}
|
||||
},
|
||||
"@tanstack/react-table": {
|
||||
"version": "8.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.7.6.tgz",
|
||||
"integrity": "sha512-/QijmMFeP7wDLBnr0MQ/5MlbXePbIL/1nOtkxBC9zvmBu4gDKJEDBqipUyM7Wc/iBpSd0IFyqBlvZvTPD9FYDA==",
|
||||
"requires": {
|
||||
"@tanstack/table-core": "8.7.6"
|
||||
}
|
||||
},
|
||||
"@tanstack/table-core": {
|
||||
"version": "8.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.7.6.tgz",
|
||||
"integrity": "sha512-sqiNTMzB6cpyL8DFH6/VqW48SwiflLqxQqYpo2wNock7rdVGvlm0BLNI8vZUJbr1+fmmWmHwBvi5OMgZw8n1DA=="
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "8.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz",
|
||||
@@ -46996,8 +47015,7 @@
|
||||
"@types/is-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.1.tgz",
|
||||
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q=="
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.4",
|
||||
@@ -47184,8 +47202,7 @@
|
||||
"@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "18.0.26",
|
||||
@@ -47310,8 +47327,7 @@
|
||||
"@types/webpack-env": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz",
|
||||
"integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg=="
|
||||
},
|
||||
"@types/webpack-sources": {
|
||||
"version": "3.2.0",
|
||||
@@ -47817,6 +47833,11 @@
|
||||
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
|
||||
"dev": true
|
||||
},
|
||||
"accept-language-parser": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
|
||||
"integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -51205,8 +51226,7 @@
|
||||
"dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"domain-browser": {
|
||||
"version": "1.2.0",
|
||||
@@ -53400,7 +53420,6 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
@@ -54282,7 +54301,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-dom/-/is-dom-1.1.0.tgz",
|
||||
"integrity": "sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-object": "^1.0.1",
|
||||
"is-window": "^1.0.2"
|
||||
@@ -54318,8 +54336,7 @@
|
||||
"is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"is-generator-fn": {
|
||||
"version": "2.1.0",
|
||||
@@ -54378,8 +54395,7 @@
|
||||
"is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA=="
|
||||
},
|
||||
"is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
@@ -54517,8 +54533,7 @@
|
||||
"is-window": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
|
||||
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg=="
|
||||
},
|
||||
"is-windows": {
|
||||
"version": "1.0.2",
|
||||
@@ -57652,8 +57667,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -57840,6 +57854,12 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"lucide-react": {
|
||||
"version": "0.105.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.105.0.tgz",
|
||||
"integrity": "sha512-iHaIkd4Wq6aNIVrFMXt3If8E/+2lnJd4WlCyntoJNIzZ8nWhdSSHWpsw7XM4rlw2319LZ2t4WLdnM8Z0ECDTOQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
@@ -57910,8 +57930,7 @@
|
||||
"map-or-similar": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz",
|
||||
"integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg=="
|
||||
},
|
||||
"map-visit": {
|
||||
"version": "1.0.0",
|
||||
@@ -58022,7 +58041,6 @@
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
|
||||
"integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"map-or-similar": "^1.5.0"
|
||||
}
|
||||
@@ -58236,7 +58254,6 @@
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
@@ -60966,8 +60983,7 @@
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"pako": {
|
||||
"version": "1.0.11",
|
||||
@@ -61253,7 +61269,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz",
|
||||
"integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.17.8"
|
||||
}
|
||||
@@ -61670,8 +61685,7 @@
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
@@ -61829,7 +61843,6 @@
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
@@ -61962,6 +61975,22 @@
|
||||
"@babel/runtime": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"react-cookies": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-cookies/-/react-cookies-0.1.1.tgz",
|
||||
"integrity": "sha512-PP75kJ4vtoHuuTdq0TAD3RmlAv7vuDQh9fkC4oDlhntgs9vX1DmREomO0Y1mcQKR9nMZ6/zxoflaMJ3MAmF5KQ==",
|
||||
"requires": {
|
||||
"cookie": "^0.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
||||
"integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-docgen": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||
@@ -62053,12 +62082,6 @@
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"react-icons": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz",
|
||||
"integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -62107,12 +62130,6 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-table": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
|
||||
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
|
||||
"requires": {}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -63591,8 +63608,7 @@
|
||||
"stable": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.6",
|
||||
@@ -63716,8 +63732,22 @@
|
||||
"store2": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz",
|
||||
"integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w=="
|
||||
},
|
||||
"storybook-addon-next-router": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook-addon-next-router/-/storybook-addon-next-router-4.0.2.tgz",
|
||||
"integrity": "sha512-0rjGAl7HziW4ecDq+VU03H1dwkw5f6phqA+PMquPzdowNVl29ejVwVadLMGlovG6x2snaxMGxtySR2c5bwegSw==",
|
||||
"requires": {
|
||||
"tslib": "2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "2.0.2",
|
||||
@@ -64058,8 +64088,7 @@
|
||||
"synchronous-promise": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.16.tgz",
|
||||
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A=="
|
||||
},
|
||||
"synckit": {
|
||||
"version": "0.8.4",
|
||||
@@ -64186,7 +64215,6 @@
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz",
|
||||
"integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/is-function": "^1.0.0",
|
||||
"global": "^4.4.0",
|
||||
@@ -64201,8 +64229,7 @@
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -64493,8 +64520,7 @@
|
||||
"ts-dedent": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
|
||||
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.1",
|
||||
@@ -65058,8 +65084,7 @@
|
||||
"uuid-browser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz",
|
||||
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg=="
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
"@next/font": "^13.1.0",
|
||||
"@prisma/client": "^4.7.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-table": "^8.7.6",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"axios": "^1.2.1",
|
||||
"boolean": "^3.2.0",
|
||||
@@ -48,6 +50,7 @@
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^6.5.1",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.105.0",
|
||||
"next": "13.0.6",
|
||||
"next-auth": "^4.18.6",
|
||||
"next-i18next": "^13.0.3",
|
||||
@@ -55,13 +58,13 @@
|
||||
"npm": "^9.2.0",
|
||||
"postcss-focus-visible": "^7.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-cookies": "^0.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-feature-flags": "^1.0.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-i18next": "^12.1.4",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.3",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"swr": "^2.0.0",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"unique-username-generator": "^1.1.3",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"about": "About",
|
||||
"account_settings": "Account",
|
||||
"admin_dashboard": "Admin Dashboard",
|
||||
"connect": "Connect",
|
||||
"conversational": "Conversational AI for everyone.",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -8,6 +9,8 @@
|
||||
"docs": "Docs",
|
||||
"github": "GitHub",
|
||||
"legal": "Legal",
|
||||
"loading": "Loading...",
|
||||
"more_information": "More Information",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"report_a_bug": "Report a Bug",
|
||||
"sign_in": "Sign In",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"grab_a_task": "Grab a task!",
|
||||
"create": "Create",
|
||||
"evaluate": "Evaluate",
|
||||
"label": "Label",
|
||||
"dashboard": "Dashboard",
|
||||
"go": "Go"
|
||||
}
|
||||
@@ -7,5 +7,8 @@
|
||||
"rank": "Rank",
|
||||
"score": "Score",
|
||||
"user": "User",
|
||||
"weekly": "Weekly"
|
||||
"weekly": "Weekly",
|
||||
"prompt": "Prompts",
|
||||
"reply": "Replies",
|
||||
"label": "Labels"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"reactions": "Reactions",
|
||||
"label_action": "Label",
|
||||
"label_title": "Label",
|
||||
"submit_labels": "Submit",
|
||||
"open_new_tab_action": "Open in new tab",
|
||||
"report_title": "Report",
|
||||
"report_action": "Report",
|
||||
"report_placeholder": "Why should this message be reviewed?",
|
||||
"send_report": "Send"
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"write_initial_prompt": "Write your prompt here...",
|
||||
"default": {
|
||||
"unchanged_title": "No changes",
|
||||
"unchanged_message": "Are you sure you would like to continue?"
|
||||
},
|
||||
"random": {
|
||||
"label": "I'm feeling lucky",
|
||||
"desc": "Help us improve Open Assistant by starting a random task."
|
||||
},
|
||||
"create_initial_prompt": {
|
||||
"label": "Create Initial Prompts",
|
||||
"desc": "Write initial prompts to help Open Assistant to try replying to diverse messages.",
|
||||
"overview": "Create an initial message to send to the assistant",
|
||||
"instruction": "Provide the initial prompts"
|
||||
},
|
||||
"reply_as_user": {
|
||||
"label": "Reply as User",
|
||||
"desc": "Chat with Open Assistant and help improve it's responses as you interact with it.",
|
||||
"overview": "Given the following conversation, provide an adequate reply",
|
||||
"instruction": "Provide the user's reply"
|
||||
},
|
||||
"reply_as_assistant": {
|
||||
"label": "Reply as Assistant",
|
||||
"desc": "Help Open Assistant improve its responses to conversations with other users.",
|
||||
"overview": "Given the following conversation, provide an adequate reply"
|
||||
},
|
||||
"rank_user_replies": {
|
||||
"label": "Rank User Replies",
|
||||
"desc": "Help Open Assistant improve its responses to conversations with other users.",
|
||||
"overview": "Given the following User replies, sort them from best to worst, best being first, worst being last.",
|
||||
"unchanged_title": "Order Unchanged",
|
||||
"unchanged_message": "You have not changed the order of the prompts. Are you sure you would like to continue?"
|
||||
},
|
||||
"rank_assistant_replies": {
|
||||
"label": "Rank Assistant Replies",
|
||||
"desc": "Score prompts given by Open Assistant based on their accuracy and readability.",
|
||||
"overview": "Given the following Assistant replies, sort them from best to worst, best being first, worst being last.",
|
||||
"unchanged_title": "Order Unchanged",
|
||||
"unchanged_message": "You have not changed the order of the prompts. Are you sure you would like to continue?"
|
||||
},
|
||||
"rank_initial_prompts": {
|
||||
"label": "Rank Initial Prompts",
|
||||
"desc": "Score prompts given by Open Assistant based on their accuracy and readability.",
|
||||
"overview": "Given the following initial prompts, sort them from best to worst, best being first, worst being last.",
|
||||
"unchanged_title": "Order Unchanged",
|
||||
"unchanged_message": "You have not changed the order of the prompts. Are you sure you would like to continue?"
|
||||
},
|
||||
"label_initial_prompt": {
|
||||
"label": "Label Initial Prompt",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Provide labels for the following prompt"
|
||||
},
|
||||
"label_prompter_reply": {
|
||||
"label": "Label Prompter Reply",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Given the following discussion, provide labels for the final prompt."
|
||||
},
|
||||
"label_assistant_reply": {
|
||||
"label": "Label Assistant Reply",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Given the following discussion, provide labels for the final prompt."
|
||||
},
|
||||
"classify_initial_prompt": {
|
||||
"label": "Classify Initial Prompt",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Read the following prompt and then answer the question about it."
|
||||
},
|
||||
"classify_prompter_reply": {
|
||||
"label": "Classify Prompter Reply",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Read the following conversation and then answer the question about the last reply in the discussion."
|
||||
},
|
||||
"classify_assistant_reply": {
|
||||
"label": "Classify Assistant Reply",
|
||||
"desc": "Provide labels for a prompt.",
|
||||
"overview": "Read the following conversation and then answer the question about the last reply in the discussion."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Radio, RadioGroup } from "@chakra-ui/react";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export const LikertButtons = ({
|
||||
isDisabled,
|
||||
count,
|
||||
onChange,
|
||||
"data-cy": dataCy,
|
||||
}: PropsWithChildren<{
|
||||
isDisabled: boolean;
|
||||
count: number;
|
||||
onChange: (value: number) => void;
|
||||
"data-cy"?: string;
|
||||
}>) => {
|
||||
const valueMap = Object.fromEntries(Array.from({ length: count }, (_, idx) => [`${idx}`, idx / (count - 1)]));
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
data-cy={dataCy}
|
||||
isDisabled={isDisabled}
|
||||
onChange={(value) => {
|
||||
onChange(valueMap[value]);
|
||||
}}
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
{Object.keys(valueMap).map((value) => {
|
||||
return <Radio key={value} value={value} data-cy="radio-option" size="md" padding="0.6em" />;
|
||||
})}
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Box, Link, Text, useColorMode } from "@chakra-ui/react";
|
||||
import { Github } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useId } from "react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
|
||||
import { Container } from "./Container";
|
||||
import { Discord } from "./Icons/Discord";
|
||||
|
||||
const CIRCLE_HEIGHT = 558;
|
||||
const CIRCLE_WIDTH = 558;
|
||||
@@ -70,7 +71,7 @@ export function CallToAction() {
|
||||
type="button"
|
||||
className="mb-2 ml-6 flex items-center rounded-md border border-transparent bg-blue-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
<FaDiscord size={25} />
|
||||
<Discord size={25} />
|
||||
<Text as="span" className="text-lg ml-3">
|
||||
{t("discord")}
|
||||
</Text>
|
||||
@@ -81,7 +82,7 @@ export function CallToAction() {
|
||||
type="button"
|
||||
className="mb-2 ml-6 flex items-center rounded-md border border-transparent bg-blue-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
<FaGithub size={25} />
|
||||
<Github size={25} />
|
||||
<Text as="span" className="text-lg ml-3">
|
||||
{t("github")}
|
||||
</Text>
|
||||
|
||||
+8
-14
@@ -1,11 +1,9 @@
|
||||
import { Box, Link, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { Card, CardBody, Link, Text } from "@chakra-ui/react";
|
||||
import NextLink from "next/link";
|
||||
import { LeaderboardGridCell } from "src/components/LeaderboardGridCell";
|
||||
import { LeaderboardTable } from "src/components/LeaderboardTable";
|
||||
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
|
||||
export function LeaderboardTable() {
|
||||
const backgroundColor = useColorModeValue("white", "gray.700");
|
||||
const accentColor = useColorModeValue("gray.200", "gray.900");
|
||||
export function LeaderboardWidget() {
|
||||
return (
|
||||
<main className="h-fit col-span-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -17,15 +15,11 @@ export function LeaderboardTable() {
|
||||
</Text>
|
||||
</Link>
|
||||
</div>
|
||||
<Box
|
||||
backgroundColor={backgroundColor}
|
||||
boxShadow="base"
|
||||
dropShadow={accentColor}
|
||||
borderRadius="xl"
|
||||
className="p-6 shadow-sm"
|
||||
>
|
||||
<LeaderboardGridCell timeFrame={LeaderboardTimeFrame.day} />
|
||||
</Box>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<LeaderboardTable timeFrame={LeaderboardTimeFrame.day} limit={5} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -1,51 +1,96 @@
|
||||
import { Box, Flex, GridItem, Heading, SimpleGrid, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
GridItem,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link as ExternalLink,
|
||||
SimpleGrid,
|
||||
Spacer,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { getTypeSafei18nKey } from "src/lib/i18n";
|
||||
import { TaskType } from "src/types/Task";
|
||||
|
||||
import { TaskCategory, TaskCategoryLabels, TaskTypes } from "../Tasks/TaskTypes";
|
||||
import { TaskCategory, TaskCategoryLabels, TaskInfo, TaskInfos } from "../Tasks/TaskTypes";
|
||||
|
||||
export const TaskOption = ({ displayTaskCategories }: { displayTaskCategories: TaskCategory[] }) => {
|
||||
export interface TasksOptionProps {
|
||||
content: Partial<Record<TaskCategory, TaskType[]>>;
|
||||
}
|
||||
|
||||
export const TaskOption = ({ content }: TasksOptionProps) => {
|
||||
const { t } = useTranslation(["dashboard", "tasks"]);
|
||||
const backgroundColor = useColorModeValue("white", "gray.700");
|
||||
|
||||
const taskInfoMap = useMemo(
|
||||
() =>
|
||||
Object.values(content)
|
||||
.flat()
|
||||
.reduce((obj, taskType) => {
|
||||
obj[taskType] = TaskInfos.filter((t) => t.type === taskType).pop();
|
||||
return obj;
|
||||
}, {} as Record<TaskType, TaskInfo>),
|
||||
[content]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-14">
|
||||
{displayTaskCategories.map((category) => (
|
||||
{Object.entries(content).map(([category, taskTypes]) => (
|
||||
<div key={category}>
|
||||
<Text className="text-2xl font-bold pb-4">{TaskCategoryLabels[category]}</Text>
|
||||
<Flex>
|
||||
<Heading size="lg" className="pb-4">
|
||||
{t(TaskCategoryLabels[category])}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<ExternalLink href="https://projects.laion.ai/Open-Assistant/" isExternal>
|
||||
<IconButton variant="ghost" aria-label="More Information" icon={<HelpCircle size="2em" />} />
|
||||
</ExternalLink>
|
||||
</Flex>
|
||||
<SimpleGrid columns={[1, 1, 2, 2, 3, 4]} gap={4}>
|
||||
{TaskTypes.filter((task) => task.category === category).map((item) => (
|
||||
<Link key={category + item.label} href={item.pathname}>
|
||||
<GridItem
|
||||
bg={backgroundColor}
|
||||
borderRadius="xl"
|
||||
boxShadow="base"
|
||||
className="flex flex-col justify-between h-full"
|
||||
>
|
||||
<Box className="p-6 pb-10">
|
||||
<Flex flexDir="column" gap="3">
|
||||
<Heading size="md" fontFamily="inter">
|
||||
{item.label}
|
||||
</Heading>
|
||||
<Text size="sm" opacity="80%">
|
||||
{item.desc}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box
|
||||
bg="blue.500"
|
||||
borderBottomRadius="xl"
|
||||
className="px-6 py-2 transition-colors duration-300"
|
||||
_hover={{ backgroundColor: "blue.600" }}
|
||||
{taskTypes
|
||||
.map((taskType) => taskInfoMap[taskType])
|
||||
.map((item) => (
|
||||
<Link key={category + item.id} href={item.pathname}>
|
||||
<GridItem
|
||||
bg={backgroundColor}
|
||||
borderRadius="xl"
|
||||
boxShadow="base"
|
||||
className="flex flex-col justify-between h-full"
|
||||
>
|
||||
<Text fontWeight="bold" color="white">
|
||||
Go ->
|
||||
<Flex className="p-6 pb-10" flexDir="column" gap="3">
|
||||
<Heading size="md">{t(getTypeSafei18nKey(`tasks:${item.id}.label`))}</Heading>
|
||||
<Text size="sm">{t(getTypeSafei18nKey(`tasks:${item.id}.desc`))}</Text>
|
||||
</Flex>
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
color="white"
|
||||
borderBottomRadius="xl"
|
||||
className="px-6 py-2 transition-colors duration-300 bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
{t("go")} ->
|
||||
</Text>
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Link>
|
||||
))}
|
||||
</GridItem>
|
||||
</Link>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const allTaskOptions: TasksOptionProps["content"] = {
|
||||
[TaskCategory.Random]: [TaskType.random],
|
||||
[TaskCategory.Create]: [TaskType.initial_prompt, TaskType.prompter_reply, TaskType.assistant_reply],
|
||||
[TaskCategory.Evaluate]: [
|
||||
TaskType.rank_initial_prompts,
|
||||
TaskType.rank_prompter_replies,
|
||||
TaskType.rank_assistant_replies,
|
||||
],
|
||||
[TaskCategory.Label]: [TaskType.label_initial_prompt, TaskType.label_prompter_reply, TaskType.label_assistant_reply],
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { LeaderboardTable } from "./LeaderboardTable";
|
||||
export { LeaderboardWidget } from "./LeaderboardWidget";
|
||||
export { TaskOption } from "./TaskOption";
|
||||
export { WelcomeCard } from "./WelcomeCard";
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Spacer,
|
||||
Table,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { Filter } from "lucide-react";
|
||||
import { ChangeEvent, ReactNode } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export type DataTableColumnDef<T> = ColumnDef<T> & {
|
||||
filterable?: boolean;
|
||||
};
|
||||
|
||||
// TODO: stricter type
|
||||
export type FilterItem = {
|
||||
id: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type DataTableProps<T> = {
|
||||
data: T[];
|
||||
columns: DataTableColumnDef<T>[];
|
||||
caption?: string;
|
||||
filterValues?: FilterItem[];
|
||||
onNextClick?: () => void;
|
||||
onPreviousClick?: () => void;
|
||||
onFilterChange?: (items: FilterItem[]) => void;
|
||||
disableNext?: boolean;
|
||||
disablePrevious?: boolean;
|
||||
disablePagination?: boolean;
|
||||
};
|
||||
|
||||
export const DataTable = <T,>({
|
||||
data,
|
||||
columns,
|
||||
caption,
|
||||
filterValues = [],
|
||||
onNextClick,
|
||||
onPreviousClick,
|
||||
onFilterChange,
|
||||
disableNext,
|
||||
disablePrevious,
|
||||
disablePagination,
|
||||
}: DataTableProps<T>) => {
|
||||
const { getHeaderGroups, getRowModel } = useReactTable<T>({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleFilterChange = (value: FilterItem) => {
|
||||
const idx = filterValues.findIndex((oldValue) => oldValue.id === value.id);
|
||||
let newValues: FilterItem[] = [];
|
||||
if (idx === -1) {
|
||||
newValues = [...filterValues, value];
|
||||
} else {
|
||||
newValues = filterValues.map((oldValue) => (oldValue.id === value.id ? value : oldValue));
|
||||
}
|
||||
onFilterChange(newValues);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!disablePagination && (
|
||||
<Flex mb="2">
|
||||
<Button onClick={onPreviousClick} disabled={disablePrevious}>
|
||||
Previous
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button onClick={onNextClick} disabled={disableNext}>
|
||||
Next
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
<TableContainer>
|
||||
<Table variant="simple">
|
||||
<TableCaption>{caption}</TableCaption>
|
||||
<Thead>
|
||||
{getHeaderGroups().map((headerGroup) => (
|
||||
<Tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Th key={header.id}>
|
||||
<Box display="flex" alignItems="center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{(header.column.columnDef as DataTableColumnDef<T>).filterable && (
|
||||
<FilterModal
|
||||
value={filterValues.find((value) => value.id === header.id)?.value ?? ""}
|
||||
onChange={(value) => handleFilterChange({ id: header.id, value })}
|
||||
label={flexRender(header.column.columnDef.header, header.getContext())}
|
||||
></FilterModal>
|
||||
)}
|
||||
</Box>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{getRowModel().rows.map((row) => (
|
||||
<Tr key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterModal = ({
|
||||
label,
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
onChange: (val: string) => void;
|
||||
value: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const handleInputChange = useDebouncedCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<PopoverTrigger>
|
||||
<Button variant={"unstyled"} ml="2">
|
||||
<Filter size="1em"></Filter>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="fit-content">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody mt="4">
|
||||
<FormControl>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Input onChange={handleInputChange} defaultValue={value}></Input>
|
||||
</FormControl>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +1,29 @@
|
||||
import { Box, Link, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { FiAlertTriangle } from "react-icons/fi";
|
||||
import { IconType } from "react-icons/lib";
|
||||
import { Box, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { AlertTriangle, LucideIcon } from "lucide-react";
|
||||
import NextLink from "next/link";
|
||||
|
||||
type EmptyStateProps = {
|
||||
text: string;
|
||||
icon: IconType;
|
||||
icon: LucideIcon;
|
||||
"data-cy"?: string;
|
||||
};
|
||||
|
||||
export const EmptyState = (props: EmptyStateProps) => {
|
||||
const backgroundColor = useColorModeValue("white", "gray.800");
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Box bg={backgroundColor} p="10" borderRadius="xl" shadow="base">
|
||||
<Box data-cy={props["data-cy"]} bg={backgroundColor} p="10" borderRadius="xl" shadow="base">
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap="8" fontSize="lg">
|
||||
<props.icon size="30" color="DarkOrange" />
|
||||
<Text>{props.text}</Text>
|
||||
<Link onClick={() => router.back()} color="blue.500" textUnderlineOffset="3px">
|
||||
<Text>Click here to go back</Text>
|
||||
</Link>
|
||||
<Text data-cy="cy-no-tasks">{props.text}</Text>
|
||||
<NextLink href="/dashboard">
|
||||
<Text color="blue.500">Go back to the dashboard</Text>
|
||||
</NextLink>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const TaskEmptyState = () => {
|
||||
return <EmptyState text="Looks like no tasks were found." icon={FiAlertTriangle} />;
|
||||
return <EmptyState text="Looks like no tasks were found." icon={AlertTriangle} data-cy="task" />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
interface ExplainProps {
|
||||
explanation: string[];
|
||||
}
|
||||
|
||||
export const Explain = ({ explanation }: ExplainProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="explanation"
|
||||
variant="link"
|
||||
size="xs"
|
||||
icon={<InformationCircleIcon className="h-4 w-4" />}
|
||||
></IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody>
|
||||
{explanation.map((paragraph, idx) => (
|
||||
<Text key={idx}>{paragraph}</Text>
|
||||
))}
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -27,10 +27,10 @@ export function Faq() {
|
||||
return (
|
||||
<ListItem className="space-y-10" key={`question_${index}`}>
|
||||
<Text as="h3" className={`text-lg font-semibold leading-6 ${headingColorClass}`}>
|
||||
{t(`faq_items.q${index}`)}
|
||||
{t(`faq_items.q${index as 0}`)}
|
||||
</Text>
|
||||
<Text as="p" className={`mt-4 text-sm ${textColorClass}`}>
|
||||
{t(`faq_items.a${index}`)}
|
||||
{t(`faq_items.a${index as 0}`)}
|
||||
</Text>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@@ -1,127 +1,69 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Slider,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
Tooltip,
|
||||
useBoolean,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
useId,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/20/solid";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { FiAlertCircle } from "react-icons/fi";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { get, post } from "src/lib/api";
|
||||
import { colors } from "src/styles/Theme/colors";
|
||||
import { Message } from "src/types/Conversation";
|
||||
import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
import { LabelInputGroup } from "./Survey/LabelInputGroup";
|
||||
|
||||
interface Label {
|
||||
name: string;
|
||||
display_text: string;
|
||||
help_text: string;
|
||||
}
|
||||
|
||||
interface LoadLabelsAction {
|
||||
type: "load_labels";
|
||||
labels: Label[];
|
||||
}
|
||||
|
||||
interface UpdateValueAction {
|
||||
type: "update_value";
|
||||
label_index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ToggleLabelAction {
|
||||
type: "toggle_label";
|
||||
label_index: number;
|
||||
check: boolean;
|
||||
}
|
||||
|
||||
interface LabelValue {
|
||||
label: Label;
|
||||
checked: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface FlagReportState {
|
||||
label_values: LabelValue[];
|
||||
submittable: boolean;
|
||||
}
|
||||
|
||||
interface FlaggableElementProps {
|
||||
children: React.ReactNode;
|
||||
message: Message;
|
||||
}
|
||||
|
||||
interface ValidLabelsResponse {
|
||||
valid_labels: Label[];
|
||||
}
|
||||
|
||||
export const FlaggableElement = (props: FlaggableElementProps) => {
|
||||
const [report, updateReport] = useReducer(
|
||||
(state: FlagReportState, action: LoadLabelsAction | UpdateValueAction | ToggleLabelAction): FlagReportState => {
|
||||
const makeState = (label_values: LabelValue[]): FlagReportState => {
|
||||
const submittable = label_values.map(({ checked }) => checked).some(Boolean);
|
||||
return { label_values, submittable };
|
||||
};
|
||||
const { data: response } = useSWRImmutable<ValidLabelsResponse>("/api/valid_labels", get);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { valid_labels } = response || { valid_labels: [] };
|
||||
const [values, setValues] = useState<number[]>([]);
|
||||
|
||||
switch (action.type) {
|
||||
case "load_labels":
|
||||
return makeState(
|
||||
action.labels.map((label) => {
|
||||
return { label, checked: false, value: 1 };
|
||||
})
|
||||
);
|
||||
case "toggle_label": {
|
||||
const values_copy = state.label_values.slice();
|
||||
values_copy[action.label_index].checked = action.check;
|
||||
return makeState(values_copy);
|
||||
}
|
||||
case "update_value": {
|
||||
const values_copy = state.label_values.slice();
|
||||
values_copy[action.label_index].value = action.value;
|
||||
return makeState(values_copy);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ label_values: [], submittable: false }
|
||||
);
|
||||
const [isEditing, setIsEditing] = useBoolean();
|
||||
|
||||
const { data, isLoading } = useSWR("/api/valid_labels", get);
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
updateReport({ type: "load_labels", labels: [] });
|
||||
return;
|
||||
}
|
||||
const { valid_labels } = data;
|
||||
updateReport({ type: "load_labels", labels: valid_labels });
|
||||
}, [data, isLoading]);
|
||||
const submittable =
|
||||
values.some((value) => {
|
||||
return value !== null;
|
||||
}) &&
|
||||
values.length === valid_labels.length &&
|
||||
valid_labels.length > 0;
|
||||
|
||||
const { trigger } = useSWRMutation("/api/set_label", post, {
|
||||
onSuccess: setIsEditing.off,
|
||||
onSuccess: onClose,
|
||||
onError: onClose,
|
||||
});
|
||||
|
||||
const submitResponse = () => {
|
||||
const label_map: Map<string, number> = new Map();
|
||||
report.label_values.forEach(({ label, checked, value }) => {
|
||||
if (checked) {
|
||||
label_map.set(label.name, value);
|
||||
console.assert(valid_labels.length === values.length);
|
||||
values.forEach((value, idx) => {
|
||||
if (value !== null) {
|
||||
label_map.set(valid_labels[idx].name, value);
|
||||
}
|
||||
});
|
||||
trigger({
|
||||
@@ -131,22 +73,8 @@ export const FlaggableElement = (props: FlaggableElementProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCheckboxState = (checked, label_index) => {
|
||||
updateReport({ type: "toggle_label", label_index, check: checked });
|
||||
};
|
||||
const handleSliderState = (value, label_index) => {
|
||||
updateReport({ type: "update_value", label_index, value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
isOpen={isEditing}
|
||||
onOpen={setIsEditing.on}
|
||||
onClose={setIsEditing.off}
|
||||
closeOnBlur={false}
|
||||
isLazy
|
||||
lazyBehavior="keepMounted"
|
||||
>
|
||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose} closeOnBlur={false} isLazy lazyBehavior="keepMounted">
|
||||
<Box display="flex" alignItems="center" flexDirection={["column", "row"]} gap="2">
|
||||
<PopoverAnchor>{props.children}</PopoverAnchor>
|
||||
|
||||
@@ -154,33 +82,24 @@ export const FlaggableElement = (props: FlaggableElementProps) => {
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<Box as="button" display="flex" alignItems="center" justifyContent="center" borderRadius="full" p="1">
|
||||
<FiAlertCircle size="20" className="text-red-400" aria-hidden="true" />
|
||||
<AlertCircle size="20" className="text-red-400" aria-hidden="true" />
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<PopoverContent width="auto" p="3" m="4" maxWidth="calc(100vw - 2rem)">
|
||||
<PopoverArrow />
|
||||
<Box className="relative h-4">
|
||||
<PopoverCloseButton />
|
||||
</Box>
|
||||
<PopoverBody>
|
||||
{report.label_values.map(({ label, checked, value }, i) => (
|
||||
<FlagCheckbox
|
||||
label={label}
|
||||
key={i}
|
||||
idx={i}
|
||||
checked={checked}
|
||||
sliderValue={value}
|
||||
checkboxHandler={handleCheckboxState}
|
||||
sliderHandler={handleSliderState}
|
||||
/>
|
||||
))}
|
||||
<Flex justify="center">
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Select one or more labels that apply.</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<LabelInputGroup labelIDs={valid_labels.map(({ name }) => name)} onChange={setValues} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
isDisabled={!report.submittable}
|
||||
isDisabled={!submittable}
|
||||
onClick={submitResponse}
|
||||
className={`bg-indigo-600 text-${useColorModeValue(
|
||||
colors.light.text,
|
||||
@@ -189,85 +108,9 @@ export const FlaggableElement = (props: FlaggableElementProps) => {
|
||||
>
|
||||
Report
|
||||
</Button>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
interface FlagCheckboxProps {
|
||||
label: Label;
|
||||
idx: number;
|
||||
checked: boolean;
|
||||
sliderValue: number;
|
||||
checkboxHandler: (newVal: boolean, idx: number) => void;
|
||||
sliderHandler: (newVal: number, idx: number) => void;
|
||||
}
|
||||
|
||||
export function FlagCheckbox(props: FlagCheckboxProps): JSX.Element {
|
||||
let AdditionalExplanation = null;
|
||||
if (props.label.help_text) {
|
||||
AdditionalExplanation = (
|
||||
<a href="#" className="text-sm inline group leading-4">
|
||||
<QuestionMarkCircleIcon
|
||||
className="h-5 w-5 ml-1 text-gray-400 group-hover:text-gray-500 inline"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const id = useId();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const labelTextClass =
|
||||
colorMode === "light"
|
||||
? `text-${colors.light.text} hover:text-blue-700`
|
||||
: `text-${colors.dark.text} hover:text-blue-400`;
|
||||
|
||||
return (
|
||||
<Flex gap="4" justifyContent="space-between" className="my-2">
|
||||
<div className="flex items-start align-middle">
|
||||
<Checkbox
|
||||
id={id}
|
||||
isChecked={props.checked}
|
||||
onChange={(e) => {
|
||||
props.checkboxHandler(e.target.checked, props.idx);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className={clsx(
|
||||
"text-sm form-check-label ml-2 break-all inline align-middle first-line:leading-4",
|
||||
labelTextClass
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
{props.label.display_text}
|
||||
{AdditionalExplanation}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!props.checked) {
|
||||
props.checkboxHandler(true, props.idx);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
width="100px"
|
||||
isDisabled={!props.checked}
|
||||
defaultValue={100}
|
||||
onChangeEnd={(val) => {
|
||||
props.sliderHandler(val / 100, props.idx);
|
||||
}}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
<SliderThumb />
|
||||
</SliderTrack>
|
||||
</Slider>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Box, Button, Flex, Text } from "@chakra-ui/react";
|
||||
import { User } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Flags } from "react-feature-flags";
|
||||
import { FaUser } from "react-icons/fa";
|
||||
import { LanguageSelector } from "src/components/LanguageSelector";
|
||||
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
@@ -16,7 +17,7 @@ function AccountButton() {
|
||||
return (
|
||||
<Link href="/auth/signin" aria-label="Home">
|
||||
<Flex alignItems="center">
|
||||
<Button variant="outline" leftIcon={<FaUser />}>
|
||||
<Button variant="outline" leftIcon={<User size={"20"} />}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Flex>
|
||||
@@ -45,6 +46,7 @@ export function Header() {
|
||||
<Flags authorizedFlags={["flagTest"]}>
|
||||
<Text>FlagTest</Text>
|
||||
</Flags>
|
||||
<LanguageSelector />
|
||||
<AccountButton />
|
||||
<UserMenu />
|
||||
</Flex>
|
||||
|
||||
@@ -11,16 +11,15 @@ import {
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { AlertTriangle, Layout, LogOut, Settings, Shield } from "lucide-react";
|
||||
import NextLink from "next/link";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { ElementType, useCallback } from "react";
|
||||
import { FiAlertTriangle, FiLayout, FiLogOut, FiSettings, FiShield } from "react-icons/fi";
|
||||
|
||||
interface MenuOption {
|
||||
name: string;
|
||||
href: string;
|
||||
desc: string;
|
||||
icon: ElementType;
|
||||
isExternal: boolean;
|
||||
}
|
||||
@@ -40,22 +39,19 @@ export function UserMenu() {
|
||||
{
|
||||
name: t("dashboard"),
|
||||
href: "/dashboard",
|
||||
desc: t("dashboard"),
|
||||
icon: FiLayout,
|
||||
icon: Layout,
|
||||
isExternal: false,
|
||||
},
|
||||
{
|
||||
name: t("account_settings"),
|
||||
href: "/account",
|
||||
desc: t("account_settings"),
|
||||
icon: FiSettings,
|
||||
icon: Settings,
|
||||
isExternal: false,
|
||||
},
|
||||
{
|
||||
name: t("report_a_bug"),
|
||||
href: "https://github.com/LAION-AI/Open-Assistant/issues/new/choose",
|
||||
desc: t("report_a_bug"),
|
||||
icon: FiAlertTriangle,
|
||||
icon: AlertTriangle,
|
||||
isExternal: true,
|
||||
},
|
||||
];
|
||||
@@ -64,8 +60,7 @@ export function UserMenu() {
|
||||
options.unshift({
|
||||
name: t("admin_dashboard"),
|
||||
href: "/admin",
|
||||
desc: t("admin_dashboard"),
|
||||
icon: FiShield,
|
||||
icon: Shield,
|
||||
isExternal: false,
|
||||
});
|
||||
}
|
||||
@@ -98,7 +93,7 @@ export function UserMenu() {
|
||||
_hover={{ textDecoration: "none" }}
|
||||
>
|
||||
<MenuItem gap="3" borderRadius="md" p="4">
|
||||
<item.icon className="text-blue-500" aria-hidden="true" />
|
||||
<item.icon size="1em" className="text-blue-500" aria-hidden="true" />
|
||||
<Text>{item.name}</Text>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
@@ -106,7 +101,7 @@ export function UserMenu() {
|
||||
</MenuGroup>
|
||||
<MenuDivider />
|
||||
<MenuItem gap="3" borderRadius="md" p="4" onClick={handleSignOut}>
|
||||
<FiLogOut className="text-blue-500" aria-hidden="true" />
|
||||
<LogOut size="1em" className="text-blue-500" aria-hidden="true" />
|
||||
<Text>{t("sign_out")}</Text>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
export const Discord: LucideIcon = ({ size = 24, ...rest }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 127.14 96.36"
|
||||
fill="currentColor"
|
||||
height={size}
|
||||
width={size}
|
||||
{...rest}
|
||||
>
|
||||
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Select } from "@chakra-ui/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import cookie from "react-cookies";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const router = useRouter();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
// Memo the set of locales and their display names.
|
||||
const localesAndNames = useMemo(() => {
|
||||
return router.locales.map((locale) => ({
|
||||
locale,
|
||||
name: new Intl.DisplayNames([locale], { type: "language" }).of(locale),
|
||||
}));
|
||||
}, [router.locales]);
|
||||
|
||||
const languageChanged = useCallback(
|
||||
async (option) => {
|
||||
const locale = option.target.value;
|
||||
cookie.save("NEXT_LOCALE", locale, { path: "/" });
|
||||
const path = router.asPath;
|
||||
return router.push(path, path, { locale });
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const { language: currentLanguage } = i18n;
|
||||
return (
|
||||
<Select onChange={languageChanged} defaultValue={currentLanguage}>
|
||||
{localesAndNames.map(({ locale, name }) => (
|
||||
<option key={locale} value={locale}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export { LanguageSelector };
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./LanguageSelector";
|
||||
@@ -1,8 +1,8 @@
|
||||
// https://nextjs.org/docs/basic-features/layouts
|
||||
|
||||
import { Box, Grid } from "@chakra-ui/react";
|
||||
import { Activity, BarChart2, Layout, MessageSquare, Users } from "lucide-react";
|
||||
import type { NextPage } from "next";
|
||||
import { FiBarChart2, FiLayout, FiMessageSquare, FiUsers, FiActivity } from "react-icons/fi";
|
||||
import { Header } from "src/components/Header";
|
||||
|
||||
import { SlimFooter } from "./Dashboard/SlimFooter";
|
||||
@@ -38,19 +38,19 @@ export const getDashboardLayout = (page: React.ReactElement) => (
|
||||
label: "Dashboard",
|
||||
pathname: "/dashboard",
|
||||
desc: "Dashboard Home",
|
||||
icon: FiLayout,
|
||||
icon: Layout,
|
||||
},
|
||||
{
|
||||
label: "Messages",
|
||||
pathname: "/messages",
|
||||
desc: "Messages Dashboard",
|
||||
icon: FiMessageSquare,
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
label: "Leaderboard",
|
||||
pathname: "/leaderboard",
|
||||
desc: "User Leaderboard",
|
||||
icon: FiBarChart2,
|
||||
icon: BarChart2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -73,13 +73,13 @@ export const getAdminLayout = (page: React.ReactElement) => (
|
||||
label: "Users",
|
||||
pathname: "/admin",
|
||||
desc: "Users Dashboard",
|
||||
icon: FiUsers,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
pathname: "/admin/status",
|
||||
desc: "Status Dashboard",
|
||||
icon: FiActivity,
|
||||
icon: Activity,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTable } from "react-table";
|
||||
import { get } from "src/lib/api";
|
||||
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
const getColumns = (t) => [
|
||||
{
|
||||
Header: t("rank"),
|
||||
accessor: "rank",
|
||||
style: { width: "90px" },
|
||||
},
|
||||
{
|
||||
Header: t("score"),
|
||||
accessor: "leader_score",
|
||||
style: { width: "90px" },
|
||||
},
|
||||
{
|
||||
Header: t("user"),
|
||||
accessor: "display_name",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Presents a grid of leaderboard entries with more detailed information.
|
||||
*/
|
||||
const LeaderboardGridCell = ({ timeFrame }: { timeFrame: LeaderboardTimeFrame }) => {
|
||||
const { t } = useTranslation(["leaderboard", "common"]);
|
||||
const { data: reply } = useSWRImmutable<LeaderboardReply>(`/api/leaderboard?time_frame=${timeFrame}`, get, {
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const columns = useMemo(() => getColumns(t), [t]);
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
|
||||
columns,
|
||||
data: reply?.leaderboard ?? [],
|
||||
});
|
||||
|
||||
const backgroundColor = useColorModeValue("white", "gray.800");
|
||||
|
||||
const lastUpdated = useMemo(() => {
|
||||
const val = new Date(reply?.last_updated);
|
||||
return t("last_updated_at", { val, formatParams: { val: { dateStyle: "full", timeStyle: "short" } } });
|
||||
}, [t, reply?.last_updated]);
|
||||
|
||||
if (!reply) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table {...getTableProps()}>
|
||||
<Thead bg={backgroundColor}>
|
||||
{headerGroups.map((headerGroup, idx) => (
|
||||
<Tr key={idx} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<Th {...column.getHeaderProps([{ style: column.style }])} key={column.id}>
|
||||
{column.render("Header")}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<Tr key={row.id} {...row.getRowProps()}>
|
||||
{row.cells.map((cell, idx) => {
|
||||
return (
|
||||
<Td key={row.id + idx} {...cell.getCellProps([{ style: cell.column.style }])}>
|
||||
{cell.render("Cell")}
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Text p="2">{lastUpdated}</Text>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { LeaderboardGridCell };
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./LeaderboardGridCell";
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CircularProgress } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { useMemo } from "react";
|
||||
import { get } from "src/lib/api";
|
||||
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import { DataTable } from "../DataTable";
|
||||
|
||||
const columnHelper = createColumnHelper<LeaderboardEntity>();
|
||||
|
||||
/**
|
||||
* Presents a grid of leaderboard entries with more detailed information.
|
||||
*/
|
||||
export const LeaderboardTable = ({ timeFrame, limit }: { timeFrame: LeaderboardTimeFrame; limit: number }) => {
|
||||
const { t } = useTranslation("leaderboard");
|
||||
const {
|
||||
data: reply,
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWRImmutable<LeaderboardReply>(`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}`, get, {
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("rank", {
|
||||
header: t("rank"),
|
||||
}),
|
||||
columnHelper.accessor("display_name", {
|
||||
header: t("user"),
|
||||
}),
|
||||
columnHelper.accessor("leader_score", {
|
||||
header: t("score"),
|
||||
}),
|
||||
columnHelper.accessor("prompts", {
|
||||
header: t("prompt"),
|
||||
}),
|
||||
columnHelper.accessor((row) => row.replies_assistant + row.replies_prompter, {
|
||||
header: t("reply"),
|
||||
}),
|
||||
columnHelper.accessor((row) => row.labels_full + row.labels_simple, {
|
||||
header: t("label"),
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const lastUpdated = useMemo(() => {
|
||||
const val = new Date(reply?.last_updated);
|
||||
return t("last_updated_at", { val, formatParams: { val: { dateStyle: "full", timeStyle: "short" } } });
|
||||
}, [t, reply?.last_updated]);
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress isIndeterminate></CircularProgress>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <span>Unable to load leaderboard</span>;
|
||||
}
|
||||
|
||||
return <DataTable data={reply.leaderboard} columns={columns} caption={lastUpdated} disablePagination></DataTable>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./LeaderboardTable";
|
||||
@@ -20,7 +20,7 @@ export const Messages = ({ messages }: MessagesProps) => {
|
||||
return <Grid gap={2}>{items}</Grid>;
|
||||
};
|
||||
|
||||
export const MessageView = forwardRef<Message, "div">((message: Message, ref) => {
|
||||
export const MessageView = forwardRef<Partial<Message>, "div">((message: Partial<Message>, ref) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const bgColor = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import { LabelInputGroup } from "src/components/Survey/LabelInputGroup";
|
||||
import { get, post } from "src/lib/api";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
interface LabelMessagePopupProps {
|
||||
messageId: string;
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface Label {
|
||||
name: string;
|
||||
display_text: string;
|
||||
help_text: string;
|
||||
}
|
||||
|
||||
interface ValidLabelsResponse {
|
||||
valid_labels: Label[];
|
||||
}
|
||||
|
||||
export const LabelMessagePopup = ({ messageId, show, onClose }: LabelMessagePopupProps) => {
|
||||
const { t } = useTranslation("message");
|
||||
const { data: response } = useSWRImmutable<ValidLabelsResponse>("/api/valid_labels", get);
|
||||
const valid_labels = response?.valid_labels ?? [];
|
||||
const [values, setValues] = useState<number[]>(null);
|
||||
|
||||
const { trigger: setLabels } = useSWRMutation("/api/set_label", post);
|
||||
|
||||
const submit = () => {
|
||||
const label_map: Map<string, number> = new Map();
|
||||
console.assert(valid_labels.length === values.length);
|
||||
values.forEach((value, idx) => {
|
||||
if (value !== null) {
|
||||
label_map.set(valid_labels[idx].name, value);
|
||||
}
|
||||
});
|
||||
setLabels({
|
||||
message_id: messageId,
|
||||
label_map: Object.fromEntries(label_map),
|
||||
});
|
||||
|
||||
setValues(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={show} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{t("label_title")}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<LabelInputGroup labelIDs={valid_labels.map(({ name }) => name)} onChange={setValues} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={submit}>
|
||||
{t("submit_labels")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
import { MessageEmojiButton } from "./MessageEmojiButton";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
title: "Messages/MessageEmojiButton",
|
||||
component: MessageEmojiButton,
|
||||
};
|
||||
|
||||
const Template = ({ emoji, count, checked }: { emoji: string; count: number; checked?: boolean }) => {
|
||||
return <MessageEmojiButton emoji={{ name: emoji, count }} checked={checked} onClick={undefined} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
emoji: "+1",
|
||||
count: 7,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const BigNumber = Template.bind({});
|
||||
BigNumber.args = {
|
||||
emoji: "+1",
|
||||
count: 999,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const Checked = Template.bind({});
|
||||
Checked.args = {
|
||||
emoji: "+1",
|
||||
count: 2,
|
||||
checked: true,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import { BoxSelect, Flag, LucideProps, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
import { ReactElement } from "react";
|
||||
import { MessageEmoji } from "src/types/Conversation";
|
||||
|
||||
type EmojiIconPurpose = "MINI_BUTTON" | "NORMAL";
|
||||
|
||||
const defaultIconProps: (purpose: EmojiIconPurpose) => LucideProps = (purpose: EmojiIconPurpose) => {
|
||||
if (purpose === "MINI_BUTTON") return { height: "1em" };
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getEmojiIcon = (name: string, purpose: EmojiIconPurpose): ReactElement => {
|
||||
switch (name) {
|
||||
case "+1":
|
||||
return <ThumbsUp {...defaultIconProps(purpose)} />;
|
||||
case "-1":
|
||||
return <ThumbsDown {...defaultIconProps(purpose)} />;
|
||||
case "flag":
|
||||
case "red_flag":
|
||||
return <Flag {...defaultIconProps(purpose)} />;
|
||||
default:
|
||||
return <BoxSelect {...defaultIconProps(purpose)} />;
|
||||
}
|
||||
};
|
||||
|
||||
interface MessageEmojiButtonProps {
|
||||
emoji: MessageEmoji;
|
||||
checked?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const MessageEmojiButton = ({ emoji, checked, onClick }: MessageEmojiButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant={checked ? "solid" : "ghost"}
|
||||
colorScheme={checked ? "blue" : undefined}
|
||||
size="sm"
|
||||
height="1.6em"
|
||||
minWidth={0}
|
||||
padding="0"
|
||||
>
|
||||
{getEmojiIcon(emoji.name, "MINI_BUTTON")}
|
||||
<span style={{ marginInlineEnd: "0.25em" }}>{emoji.count}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { Message } from "src/types/Conversation";
|
||||
|
||||
import { MessageTable } from "./MessageTable";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
title: "Messages/MessageTable",
|
||||
component: MessageTable,
|
||||
};
|
||||
|
||||
const Template = ({
|
||||
messages,
|
||||
enableLink,
|
||||
highlightLastMessage,
|
||||
}: {
|
||||
messages: Message[];
|
||||
enableLink: boolean;
|
||||
highlightLastMessage: boolean;
|
||||
}) => {
|
||||
return <MessageTable messages={messages} enableLink={enableLink} highlightLastMessage={highlightLastMessage} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
messages: [
|
||||
{
|
||||
text: "I'm unsure how to interpret this. Is it a riddle?",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
{
|
||||
text: "No, I just wanted to see how you reply when I type random characters. Can you tell me who invented Wikipedia?",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: { "-1": 11, red_flag: 2 },
|
||||
user_emojis: [],
|
||||
},
|
||||
{
|
||||
text: "Sorry, my cat sat on my keyboard. Can you print a cat in ASCII art?",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
],
|
||||
enableLink: true,
|
||||
highlightLastMessage: false,
|
||||
};
|
||||
|
||||
export const Conversation = Template.bind({});
|
||||
Conversation.args = {
|
||||
messages: [
|
||||
{
|
||||
text: "Hello! How can I help you?",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
{
|
||||
text: "Who were the 8 presidents before George Washington?",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
],
|
||||
enableLink: false,
|
||||
highlightLastMessage: true,
|
||||
};
|
||||
|
||||
export const LongText = Template.bind({});
|
||||
LongText.args = {
|
||||
messages: [
|
||||
{
|
||||
text: "Okay, here\u2019s my answer. The thing is, in a job interview you are applying to the position, right? Which means that you need to try to make yourself sound as good a fit as possible. So that means it\u2019s a mistake to wear something that people would think of as a silly or stupid outfit. It\u2019s good to wear something in line with what people expect the job to require, and ideally you want to wear something that you can walk around comfortably in. At the same time, if you wear something that looks kind of dorky, you might not stand out as much as someone who doesn\u2019t, because the other candidates probably all have something slightly weird or wacky about them too. So, I\u2019d say, do you like your outfit? Wear something that you\u2019re comfortable in, that you think you look good in, and is in line with what the job would require. Also, for interview prep, I would focus on making yourself look as professional as possible, from your hair to your outfit. What do you think of this?",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
{
|
||||
text: "Yes, I think they can be helpful when the child misbehaves, but they should be used with a little bit of compassion and understanding that it\u2019s not the natural state of things to have an adult yelling at them. Time outs are also often used without letting the child know how they\u2019re getting out of the time out, which can make it feel arbitrary or like a punishment, rather than a consequence for something they did. It\u2019s really easy for adults to do this kind of thing unconsciously. It\u2019s easy to get caught up in the notion that \u201cThey\u2019re in time out, and that\u2019s the end of it!\u201d but kids can be pretty imaginative, and they can use their own creativity to make their way out of time outs. A compassionate time out ends when the child shows a sign of understanding what they\u2019ve done wrong, and are ready to begin again. That way the child knows they\u2019re learning, and that the parent is seeing them as an intelligent person, even if they sometimes mess up. You can still use the other techniques you were using to be tough when necessary, but using a compassionate approach will let you use them without actually using them!",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
{
|
||||
text: "No. The USA was founded by a Puritan group of Protestants, but it didn\u2019t adopt the religion of the Puritans until much later, and it was always a secular state. The Puritans observed the Sabbath on Sunday, and the Puritans only had a small influence in the early history of the USA. It\u2019s difficult to trace the origins of closing stores on Sunday, but one early and short-lived attempt at forcing the Sabbath on people in the 1800s was motivated by the Protestant ideal that people should spend Sunday focusing on spiritual activities. By the mid-1800s, when the Sunday closing law was made, there was not a lot of pressure from that standpoint, but the church had begun to advocate for Sunday closing laws as a way of counteracting the negative effects of industrialization on the day of rest. Even after that shift, closing stores on Sunday was not always possible, since the religious Sunday was not always chosen for observance. And as industrialization accelerated and mechanization made it possible to operate stores on Sunday, the law was not enforced as much as people liked. The day of rest was also being violated by stores that stayed open all day on Sunday, so closing stores on Sundays became an effort to protect the Sabbath for all citizens.",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
},
|
||||
],
|
||||
enableLink: true,
|
||||
highlightLastMessage: false,
|
||||
};
|
||||
@@ -5,13 +5,19 @@ import { Message } from "src/types/Conversation";
|
||||
interface MessageTableProps {
|
||||
messages: Message[];
|
||||
enableLink?: boolean;
|
||||
highlightLastMessage?: boolean;
|
||||
}
|
||||
|
||||
export function MessageTable({ messages, enableLink }: MessageTableProps) {
|
||||
export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) {
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
{messages.map((item) => (
|
||||
<MessageTableEntry enabled={enableLink} item={item} key={item.id + item.frontend_message_id} />
|
||||
{messages.map((message, idx) => (
|
||||
<MessageTableEntry
|
||||
enabled={enableLink}
|
||||
message={message}
|
||||
key={message.id + message.frontend_message_id}
|
||||
highlight={highlightLastMessage && idx === messages.length - 1}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { Message } from "src/types/Conversation";
|
||||
|
||||
import { MessageTableEntry } from "./MessageTableEntry";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
title: "Messages/MessageTableEntry",
|
||||
component: MessageTableEntry,
|
||||
};
|
||||
|
||||
const Template = ({ enabled, highlight, ...message }) => {
|
||||
return <MessageTableEntry message={message as Message} enabled={enabled} highlight={highlight} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
text: "Who were the 8 presidents before George Washington?",
|
||||
is_assistant: false,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
};
|
||||
|
||||
export const Asistant = Template.bind({});
|
||||
Asistant.args = {
|
||||
text: "Who were the 8 presidents before George Washington?",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
};
|
||||
|
||||
export const LongText = Template.bind({});
|
||||
LongText.args = {
|
||||
text: "Assistant: No. The USA was founded by a Puritan group of Protestants, but it didn\u2019t adopt the religion of the Puritans until much later, and it was always a secular state. The Puritans observed the Sabbath on Sunday, and the Puritans only had a small influence in the early history of the USA. It\u2019s difficult to trace the origins of closing stores on Sunday, but one early and short-lived attempt at forcing the Sabbath on people in the 1800s was motivated by the Protestant ideal that people should spend Sunday focusing on spiritual activities. By the mid-1800s, when the Sunday closing law was made, there was not a lot of pressure from that standpoint, but the church had begun to advocate for Sunday closing laws as a way of counteracting the negative effects of industrialization on the day of rest. Even after that shift, closing stores on Sunday was not always possible, since the religious Sunday was not always chosen for observance. And as industrialization accelerated and mechanization made it possible to operate stores on Sunday, the law was not enforced as much as people liked. The day of rest was also being violated by stores that stayed open all day on Sunday, so closing stores on Sundays became an effort to protect the Sabbath for all citizens.",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
};
|
||||
|
||||
export const WithEmoji = Template.bind({});
|
||||
WithEmoji.args = {
|
||||
text: "As you\u2019ve mentioned, Star Wars has many sequels, prequels, and crossovers. The official list of movies in Star Wars is:",
|
||||
is_assistant: true,
|
||||
id: "",
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: { "-1": 5, "+1": 1 },
|
||||
user_emojis: ["-1"],
|
||||
};
|
||||
@@ -1,22 +1,47 @@
|
||||
import { Avatar, Box, HStack, LinkBox, useBreakpoint, useBreakpointValue, useColorModeValue } from "@chakra-ui/react";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
HStack,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
SimpleGrid,
|
||||
useBreakpointValue,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { boolean } from "boolean";
|
||||
import Link from "next/link";
|
||||
import { ClipboardList, Flag, MessageSquare, MoreHorizontal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { FlaggableElement } from "src/components/FlaggableElement";
|
||||
import { Message } from "src/types/Conversation";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { LabelMessagePopup } from "src/components/Messages/LabelPopup";
|
||||
import { getEmojiIcon, MessageEmojiButton } from "src/components/Messages/MessageEmojiButton";
|
||||
import { ReportPopup } from "src/components/Messages/ReportPopup";
|
||||
import { post } from "src/lib/api";
|
||||
import { Message, MessageEmojis } from "src/types/Conversation";
|
||||
import { colors } from "styles/Theme/colors";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
interface MessageTableEntryProps {
|
||||
item: Message;
|
||||
message: Message;
|
||||
enabled?: boolean;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export function MessageTableEntry(props: MessageTableEntryProps) {
|
||||
export function MessageTableEntry({ message, enabled, highlight }: MessageTableEntryProps) {
|
||||
const router = useRouter();
|
||||
const [emojiState, setEmojis] = useState<MessageEmojis>({ emojis: {}, user_emojis: [] });
|
||||
useEffect(() => {
|
||||
setEmojis({ emojis: message.emojis, user_emojis: message.user_emojis });
|
||||
}, [message.emojis, message.user_emojis]);
|
||||
|
||||
const { item } = props;
|
||||
|
||||
const goToMessage = useCallback(() => router.push(`/messages/${item.id}`), [router, item.id]);
|
||||
const goToMessage = useCallback(() => router.push(`/messages/${message.id}`), [router, message.id]);
|
||||
const { isOpen: reportPopupOpen, onOpen: showReportPopup, onClose: closeReportPopup } = useDisclosure();
|
||||
const { isOpen: labelPopupOpen, onOpen: showLabelPopup, onClose: closeLabelPopup } = useDisclosure();
|
||||
|
||||
const backgroundColor = useColorModeValue("gray.100", "gray.700");
|
||||
const backgroundColor2 = useColorModeValue("#DFE8F1", "#42536B");
|
||||
@@ -31,31 +56,124 @@ export function MessageTableEntry(props: MessageTableEntryProps) {
|
||||
borderColor={borderColor}
|
||||
size={inlineAvatar ? "xs" : "sm"}
|
||||
mr={inlineAvatar ? 2 : 0}
|
||||
name={`${boolean(item.is_assistant) ? "Assistant" : "User"}`}
|
||||
src={`${boolean(item.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`}
|
||||
name={`${boolean(message.is_assistant) ? "Assistant" : "User"}`}
|
||||
src={`${boolean(message.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`}
|
||||
/>
|
||||
),
|
||||
[borderColor, inlineAvatar, item.is_assistant]
|
||||
[borderColor, inlineAvatar, message.is_assistant]
|
||||
);
|
||||
const highlightColor = useColorModeValue(colors.light.highlight, colors.dark.highlight);
|
||||
|
||||
const { trigger: sendEmojiChange } = useSWRMutation(`/api/messages/${message.id}/emoji`, post, {
|
||||
onSuccess: setEmojis,
|
||||
});
|
||||
const react = (emoji: string, state: boolean) => {
|
||||
sendEmojiChange({ op: state ? "add" : "remove", emoji });
|
||||
};
|
||||
|
||||
return (
|
||||
<FlaggableElement message={item}>
|
||||
<HStack w={["full", "full", "full", "fit-content"]} gap={2}>
|
||||
{!inlineAvatar && avatar}
|
||||
<Box
|
||||
width={["full", "full", "full", "fit-content"]}
|
||||
maxWidth={["full", "full", "full", "2xl"]}
|
||||
p="4"
|
||||
borderRadius="md"
|
||||
bg={item.is_assistant ? backgroundColor : backgroundColor2}
|
||||
onClick={props.enabled && goToMessage}
|
||||
_hover={props.enabled && { cursor: "pointer", opacity: 0.9 }}
|
||||
whiteSpace="pre-wrap"
|
||||
<HStack w={["full", "full", "full", "fit-content"]} gap={2}>
|
||||
{!inlineAvatar && avatar}
|
||||
<Box
|
||||
width={["full", "full", "full", "fit-content"]}
|
||||
maxWidth={["full", "full", "full", "2xl"]}
|
||||
p="4"
|
||||
borderRadius="md"
|
||||
bg={message.is_assistant ? backgroundColor : backgroundColor2}
|
||||
outline={highlight && "2px solid black"}
|
||||
outlineColor={highlightColor}
|
||||
onClick={enabled && goToMessage}
|
||||
whiteSpace="pre-wrap"
|
||||
cursor={enabled && "pointer"}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
{inlineAvatar && avatar}
|
||||
{message.text}
|
||||
<HStack
|
||||
style={{ float: "right", position: "relative", right: "-0.3em", bottom: "-0em", marginLeft: "1em" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{inlineAvatar && avatar}
|
||||
{item.text}
|
||||
</Box>
|
||||
</HStack>
|
||||
</FlaggableElement>
|
||||
{Object.entries(emojiState.emojis).map(([emoji, count]) => (
|
||||
<MessageEmojiButton
|
||||
key={emoji}
|
||||
emoji={{ name: emoji, count }}
|
||||
checked={emojiState.user_emojis.includes(emoji)}
|
||||
onClick={() => react(emoji, !emojiState.user_emojis.includes(emoji))}
|
||||
/>
|
||||
))}
|
||||
<MessageActions
|
||||
react={react}
|
||||
userEmoji={emojiState.user_emojis}
|
||||
onLabel={showLabelPopup}
|
||||
onReport={showReportPopup}
|
||||
messageId={message.id}
|
||||
/>
|
||||
<LabelMessagePopup messageId={message.id} show={labelPopupOpen} onClose={closeLabelPopup} />
|
||||
<ReportPopup messageId={message.id} show={reportPopupOpen} onClose={closeReportPopup} />
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
const EmojiMenuItem = ({
|
||||
emoji,
|
||||
checked,
|
||||
react,
|
||||
}: {
|
||||
emoji: string;
|
||||
checked?: boolean;
|
||||
react: (emoji: string, state: boolean) => void;
|
||||
}) => {
|
||||
const activeColor = useColorModeValue(colors.light.active, colors.dark.active);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => react(emoji, !checked)} justifyContent="center" color={checked ? activeColor : undefined}>
|
||||
{getEmojiIcon(emoji, "NORMAL")}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageActions = ({
|
||||
react,
|
||||
userEmoji,
|
||||
onLabel,
|
||||
onReport,
|
||||
messageId,
|
||||
}: {
|
||||
react: (emoji: string, state: boolean) => void;
|
||||
userEmoji: string[];
|
||||
onLabel: () => void;
|
||||
onReport: () => void;
|
||||
messageId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation("message");
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<MoreHorizontal />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuGroup title={t("reactions")}>
|
||||
<SimpleGrid columns={4}>
|
||||
{["+1", "-1"].map((emoji) => (
|
||||
<EmojiMenuItem key={emoji} emoji={emoji} checked={userEmoji.includes(emoji)} react={react} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</MenuGroup>
|
||||
<MenuDivider />
|
||||
<MenuItem onClick={onLabel} icon={<ClipboardList />}>
|
||||
{t("label_action")}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onReport} icon={<Flag />}>
|
||||
{t("report_action")}
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem as="a" href={`/messages/${messageId}`} target="_blank" icon={<MessageSquare />}>
|
||||
{t("open_new_tab_action")}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export function MessageWithChildren(props: MessageWithChildrenProps) {
|
||||
{isFirst ? "Message" : depth === 1 ? "Children" : "Ancestor"}
|
||||
</Text>
|
||||
<Box width="fit-content" bg={backgroundColor} padding="4" borderRadius="xl" boxShadow="base">
|
||||
<MessageTableEntry enabled item={message} />
|
||||
<MessageTableEntry enabled message={message} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
@@ -86,9 +86,9 @@ export function MessageWithChildren(props: MessageWithChildrenProps) {
|
||||
gap="4"
|
||||
shadow="base"
|
||||
>
|
||||
{children.map((item, idx) => (
|
||||
{children.map((message, idx) => (
|
||||
<Box flex="1" key={`recursiveMessageWChildren_${idx}`}>
|
||||
<MessageTableEntry enabled item={item} />
|
||||
<MessageTableEntry enabled message={message} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Textarea,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import { post } from "src/lib/api";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
interface ReportPopupProps {
|
||||
messageId: string;
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ReportPopup = ({ messageId, show, onClose }: ReportPopupProps) => {
|
||||
const { t } = useTranslation("message");
|
||||
const [text, setText] = useState("");
|
||||
const { trigger } = useSWRMutation("/api/report", post);
|
||||
|
||||
const submit = () => {
|
||||
trigger({
|
||||
message_id: messageId,
|
||||
text,
|
||||
});
|
||||
|
||||
setText("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={show} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{t("report_title")}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Textarea onChange={(e) => setText(e.target.value)} resize="none" placeholder={t("report_placeholder")} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={submit}>
|
||||
{t("send_report")}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Box, Button, Text, Tooltip, useColorMode } from "@chakra-ui/react";
|
||||
import { LucideIcon, Sun } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FiSun } from "react-icons/fi";
|
||||
import { IconType } from "react-icons/lib";
|
||||
import { colors } from "styles/Theme/colors";
|
||||
|
||||
export interface MenuButtonOption {
|
||||
label: string;
|
||||
pathname: string;
|
||||
desc: string;
|
||||
icon: IconType;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
export interface SideMenuProps {
|
||||
@@ -47,7 +46,7 @@ export function SideMenu(props: SideMenuProps) {
|
||||
bg={router.pathname === item.pathname ? "blue.500" : null}
|
||||
_hover={router.pathname === item.pathname ? { bg: "blue.600" } : null}
|
||||
>
|
||||
<item.icon className={router.pathname === item.pathname ? "text-blue-200" : null} />
|
||||
<item.icon size={"1em"} className={router.pathname === item.pathname ? "text-blue-200" : null} />
|
||||
<Text
|
||||
fontWeight="normal"
|
||||
color={router.pathname === item.pathname ? "white" : null}
|
||||
@@ -63,7 +62,7 @@ export function SideMenu(props: SideMenuProps) {
|
||||
<div>
|
||||
<Tooltip fontFamily="inter" label="Toggle Dark Mode" placement="right" className="hidden lg:hidden sm:block">
|
||||
<Button size="lg" width="full" justifyContent="center" onClick={toggleColorMode} gap="2">
|
||||
<FiSun />
|
||||
<Sun size={"1em"} />
|
||||
<Text fontWeight="normal" className="hidden lg:block">
|
||||
{colorMode === "light" ? "Dark Mode" : "Light Mode"}
|
||||
</Text>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { PropsWithChildren, useState } from "react";
|
||||
import { RxDragHandleDots2 } from "react-icons/rx";
|
||||
|
||||
export const SortableItem = ({
|
||||
children,
|
||||
@@ -45,7 +45,7 @@ export const SortableItem = ({
|
||||
style={style}
|
||||
shadow="base"
|
||||
>
|
||||
<Box pr="4">{isEditable ? <RxDragHandleDots2 size="20px" /> : `${index + 1}.`}</Box>
|
||||
<Box pr="4">{isEditable ? <GripVertical size="20px" /> : `${index + 1}.`}</Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { Box, Grid, GridItem, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { LikertButtons } from "src/components/Buttons/LikertButtons";
|
||||
import { Explain } from "src/components/Explain";
|
||||
|
||||
interface LabelInputGroupProps {
|
||||
labelIDs: Array<string>;
|
||||
onChange: (values: number[]) => unknown;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
interface LabelInfo {
|
||||
zeroText: string;
|
||||
oneText: string;
|
||||
zeroDescription: string[];
|
||||
oneDescription: string[];
|
||||
inverted: boolean;
|
||||
}
|
||||
|
||||
const getLabelInfo = (label: string): LabelInfo => {
|
||||
switch (label) {
|
||||
case "spam":
|
||||
return {
|
||||
zeroText: "Not Spam",
|
||||
zeroDescription: ["Suitable for training Open Assistant."],
|
||||
oneText: "Spam",
|
||||
oneDescription: [
|
||||
"Seems to be intentionally low-quality or irrelevant",
|
||||
'We consider the following unwanted content as spam: trolling, intentional undermining of our purpose, illegal material, material that violates our code of conduct, and other things that are inappropriate for our dataset. We collect these under the common heading of "spam".',
|
||||
"This is not an assessment of whether this message is the best possible answer. Especially for prompts or user-replies, we very much want to retain all kinds of responses in the dataset, so that the assistant can learn to reply appropriately.",
|
||||
"Please mark this text as spam only if it is clearly unsuited to be part of our dataset, as outlined above, and try not to make any subjective value-judgments beyond that.",
|
||||
],
|
||||
inverted: true,
|
||||
};
|
||||
case "fails_task":
|
||||
return {
|
||||
zeroText: "Follows Instructions",
|
||||
zeroDescription: [],
|
||||
oneText: "Fails Task",
|
||||
oneDescription: ["Fails to follow the correct instruction / task"],
|
||||
inverted: true,
|
||||
};
|
||||
case "not_appropriate":
|
||||
return {
|
||||
zeroText: "Appropriate",
|
||||
zeroDescription: [],
|
||||
oneText: "Inappropriate",
|
||||
oneDescription: ["Inappropriate for customer assistant"],
|
||||
inverted: true,
|
||||
};
|
||||
case "violence":
|
||||
return {
|
||||
zeroText: "Harmless",
|
||||
zeroDescription: [],
|
||||
oneText: "Violent",
|
||||
oneDescription: ["Encourages or fails to discourage violence/abuse/terrorism/self-harm"],
|
||||
inverted: true,
|
||||
};
|
||||
case "excessive_harm":
|
||||
return {
|
||||
zeroText: "Safe",
|
||||
zeroDescription: [],
|
||||
oneText: "Harmful",
|
||||
oneDescription: [
|
||||
"Content likely to cause excessive harm not justifiable in the context",
|
||||
"Harm refers to physical or mental damage or injury to someone or something. Excessive refers to a reasonable threshold of harm in the context, for instance damaging skin is not excessive in the context of surgery.",
|
||||
],
|
||||
inverted: true,
|
||||
};
|
||||
case "sexual_content":
|
||||
return {
|
||||
zeroText: "Non Sexual",
|
||||
zeroDescription: [],
|
||||
oneText: "Sexual",
|
||||
oneDescription: ["Contains sexual content"],
|
||||
inverted: true,
|
||||
};
|
||||
case "toxicity":
|
||||
return {
|
||||
zeroText: "Polite",
|
||||
zeroDescription: [],
|
||||
oneText: "Rude",
|
||||
oneDescription: ["Contains rude, abusive, profane or insulting content"],
|
||||
inverted: true,
|
||||
};
|
||||
case "moral_judgement":
|
||||
return {
|
||||
zeroText: "Non-Judgemental",
|
||||
zeroDescription: [],
|
||||
oneText: "Judgemental",
|
||||
oneDescription: ["Expresses moral judgement"],
|
||||
inverted: true,
|
||||
};
|
||||
case "political_content":
|
||||
return {
|
||||
zeroText: "Apolitical",
|
||||
zeroDescription: [],
|
||||
oneText: "Political",
|
||||
oneDescription: ["Expresses political views"],
|
||||
inverted: true,
|
||||
};
|
||||
case "humor":
|
||||
return {
|
||||
zeroText: "Serious",
|
||||
zeroDescription: [],
|
||||
oneText: "Humorous",
|
||||
oneDescription: ["Contains humorous content including sarcasm"],
|
||||
inverted: false,
|
||||
};
|
||||
case "hate_speech":
|
||||
return {
|
||||
zeroText: "Safe",
|
||||
zeroDescription: [],
|
||||
oneText: "Hateful",
|
||||
oneDescription: [
|
||||
"Content is abusive or threatening and expresses prejudice against a protected characteristic",
|
||||
"Prejudice refers to preconceived views not based on reason. Protected characteristics include gender, ethnicity, religion, sexual orientation, and similar characteristics.",
|
||||
],
|
||||
inverted: true,
|
||||
};
|
||||
case "threat":
|
||||
return {
|
||||
zeroText: "Safe",
|
||||
zeroDescription: [],
|
||||
oneText: "Threatening",
|
||||
oneDescription: ["Contains a threat against a person or persons"],
|
||||
inverted: true,
|
||||
};
|
||||
case "misleading":
|
||||
return {
|
||||
zeroText: "Accurate",
|
||||
zeroDescription: [],
|
||||
oneText: "Misleading",
|
||||
oneDescription: ["Contains text which is incorrect or misleading"],
|
||||
inverted: true,
|
||||
};
|
||||
case "helpful":
|
||||
return {
|
||||
zeroText: "Unhelful",
|
||||
zeroDescription: [],
|
||||
oneText: "Helpful",
|
||||
oneDescription: ["Completes the task to a high standard"],
|
||||
inverted: false,
|
||||
};
|
||||
case "creative":
|
||||
return {
|
||||
zeroText: "Boring",
|
||||
zeroDescription: [],
|
||||
oneText: "Creative",
|
||||
oneDescription: ["Expresses creativity in responding to the task"],
|
||||
inverted: false,
|
||||
};
|
||||
case "pii":
|
||||
return {
|
||||
zeroText: "Clean",
|
||||
zeroDescription: [],
|
||||
oneText: "Contains PII",
|
||||
oneDescription: ["Contains personally identifing information"],
|
||||
inverted: false,
|
||||
};
|
||||
case "quality":
|
||||
return {
|
||||
zeroText: "Low Quality",
|
||||
zeroDescription: [],
|
||||
oneText: "High Quality",
|
||||
oneDescription: [],
|
||||
inverted: false,
|
||||
};
|
||||
case "creativity":
|
||||
return {
|
||||
zeroText: "Ordinary",
|
||||
zeroDescription: [],
|
||||
oneText: "Creative",
|
||||
oneDescription: [],
|
||||
inverted: false,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
zeroText: `!${label}`,
|
||||
zeroDescription: [],
|
||||
oneText: label,
|
||||
oneDescription: [],
|
||||
inverted: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const LabelInputGroup = ({ labelIDs, onChange, isEditable = true }: LabelInputGroupProps) => {
|
||||
const [labelValues, setLabelValues] = useState<number[]>(Array.from({ length: labelIDs.length }).map(() => null));
|
||||
|
||||
const cardColor = useColorModeValue("gray.50", "gray.800");
|
||||
|
||||
return (
|
||||
<Grid templateColumns={"minmax(min-content, 30em)"} rowGap={2}>
|
||||
{labelIDs.map((labelId, idx) => {
|
||||
const { zeroText, oneText, zeroDescription, oneDescription, inverted } = getLabelInfo(labelId);
|
||||
|
||||
let textA = zeroText;
|
||||
let textB = oneText;
|
||||
let descriptionA = zeroDescription;
|
||||
let descriptionB = oneDescription;
|
||||
if (inverted) [textA, textB, descriptionA, descriptionB] = [textB, textA, descriptionB, descriptionA];
|
||||
|
||||
return (
|
||||
<Box key={idx} padding={2} bg={cardColor} borderRadius="md" position="relative">
|
||||
<Grid
|
||||
templateColumns={{
|
||||
base: "minmax(0, 1fr) minmax(0, 1fr)",
|
||||
sm: "minmax(0, 1fr) auto minmax(0, 1fr)",
|
||||
}}
|
||||
alignItems="center"
|
||||
>
|
||||
<Text as="div">
|
||||
{textA}
|
||||
{descriptionA.length > 0 ? <Explain explanation={descriptionA} /> : null}
|
||||
</Text>
|
||||
<GridItem colSpan={{ base: 2, sm: 1 }} gridColumnStart={{ base: 1, sm: 2 }} gridRow={{ base: 2, sm: 1 }}>
|
||||
<LikertButtons
|
||||
isDisabled={!isEditable}
|
||||
count={5}
|
||||
data-cy="label-options"
|
||||
onChange={(value) => {
|
||||
const newState = labelValues.slice();
|
||||
newState[idx] = value === null ? null : inverted ? 1 - value : value;
|
||||
onChange(newState);
|
||||
setLabelValues(newState);
|
||||
}}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text textAlign="right" as="div">
|
||||
{textB}
|
||||
{descriptionB.length > 0 ? <Explain explanation={descriptionB} /> : null}
|
||||
</Text>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
useColorMode,
|
||||
} from "@chakra-ui/react";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useId, useState } from "react";
|
||||
import { colors } from "src/styles/Theme/colors";
|
||||
|
||||
interface LabelRadioGroupProps {
|
||||
labelIDs: Array<string>;
|
||||
onChange: (sliderValues: number[]) => unknown;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
const label_messages: { [label: string]: { description: string; explanation: string[] } } = {
|
||||
spam: {
|
||||
description: "Is the message spam?",
|
||||
explanation: [
|
||||
'We consider the following unwanted content as spam: trolling, intentional undermining of our purpose, illegal material, material that violates our code of conduct, and other things that are inappropriate for our dataset. We collect these under the common heading of "spam".',
|
||||
"This is not an assessment of whether this message is the best possible answer. Especially for prompts or user-replies, we very much want to retain all kinds of responses in the dataset, so that the assistant can learn to reply appropriately.",
|
||||
"Please mark this text as spam only if it is clearly unsuited to be part of our dataset, as outlined above, and try not to make any subjective value-judgments beyond that.",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelRadioGroup = (props: LabelRadioGroupProps) => {
|
||||
const [labelValues, setLabelValues] = useState<number[]>(Array.from({ length: props.labelIDs.length }).map(() => 0));
|
||||
const [interactionFlag, setInteractionFlag] = useState(false);
|
||||
|
||||
return (
|
||||
<Flex direction="column" justify="center">
|
||||
{props.labelIDs.map((labelId, idx) => (
|
||||
<LabelRadioItem
|
||||
key={idx}
|
||||
labelText={label_messages[labelId] || { description: labelId }}
|
||||
labelValue={labelValues[idx]}
|
||||
clickHandler={(newValue) => {
|
||||
const newState = labelValues.slice();
|
||||
newState[idx] = newValue;
|
||||
props.onChange(newState);
|
||||
setLabelValues(newState);
|
||||
if (!interactionFlag) setInteractionFlag(true);
|
||||
}}
|
||||
states={[
|
||||
{ text: "No", value: 0 },
|
||||
{ text: "Yes", value: 1 },
|
||||
]}
|
||||
isEditable={props.isEditable}
|
||||
interactionFlag={interactionFlag}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
interface ButtonState {
|
||||
text: string;
|
||||
value: number;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
interface LabelRadioItemProps {
|
||||
labelText: { description: string; explanation?: string[] };
|
||||
labelValue: number;
|
||||
clickHandler: (newVal: number) => unknown;
|
||||
states: ButtonState[];
|
||||
isEditable: boolean;
|
||||
interactionFlag: boolean;
|
||||
}
|
||||
|
||||
const LabelRadioItem = (props: LabelRadioItemProps) => {
|
||||
const id = useId();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const labelTextClass = colorMode === "light" ? `text-${colors.light.text}` : `text-${colors.dark.text}`;
|
||||
|
||||
return (
|
||||
<Box data-cy="label-group-item" data-label-type="radio">
|
||||
<label className="text-sm" htmlFor={id}>
|
||||
{/* TODO: display real text instead of just the id */}
|
||||
<span className={labelTextClass}>{props.labelText.description}</span>
|
||||
{props.labelText.explanation ? (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="explanation"
|
||||
variant="link"
|
||||
icon={<InformationCircleIcon className="h-5 w-5" />}
|
||||
></IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody>
|
||||
{props.labelText.explanation.map((paragraph, idx) => (
|
||||
<Text key={idx}>{paragraph}</Text>
|
||||
))}
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null}
|
||||
</label>
|
||||
<Flex direction="row" gap={6} justify="center">
|
||||
{props.states.map((item, idx) => (
|
||||
<Button
|
||||
aria-roledescription="radio-button"
|
||||
colorScheme={item.value === props.labelValue && props.interactionFlag ? item.colorScheme || "blue" : "gray"}
|
||||
isDisabled={!props.isEditable}
|
||||
size="lg"
|
||||
key={idx}
|
||||
onClick={() => props.clickHandler(item.value)}
|
||||
>
|
||||
{item.text}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Grid, Slider, SliderFilledTrack, SliderThumb, SliderTrack, useColorMode } from "@chakra-ui/react";
|
||||
import { useId, useState } from "react";
|
||||
import { colors } from "src/styles/Theme/colors";
|
||||
|
||||
// TODO: consolidate with FlaggableElement
|
||||
interface LabelSliderGroupProps {
|
||||
labelIDs: Array<string>;
|
||||
onChange: (sliderValues: number[]) => unknown;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
export const LabelSliderGroup = ({ labelIDs, onChange, isEditable }: LabelSliderGroupProps) => {
|
||||
const [sliderValues, setSliderValues] = useState<number[]>(Array.from({ length: labelIDs.length }).map(() => 0));
|
||||
|
||||
return (
|
||||
<Grid templateColumns="auto 1fr" rowGap={1} columnGap={4}>
|
||||
{labelIDs.map((labelId, idx) => (
|
||||
<CheckboxSliderItem
|
||||
key={idx}
|
||||
labelId={labelId}
|
||||
sliderValue={sliderValues[idx]}
|
||||
sliderHandler={(sliderValue) => {
|
||||
const newState = sliderValues.slice();
|
||||
newState[idx] = sliderValue;
|
||||
onChange(newState);
|
||||
setSliderValues(newState);
|
||||
}}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
function CheckboxSliderItem(props: {
|
||||
labelId: string;
|
||||
sliderValue: number;
|
||||
sliderHandler: (newVal: number) => unknown;
|
||||
isEditable: boolean;
|
||||
}) {
|
||||
const id = useId();
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const labelTextClass = colorMode === "light" ? `text-${colors.light.text}` : `text-${colors.dark.text}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="text-sm" htmlFor={id}>
|
||||
{/* TODO: display real text instead of just the id */}
|
||||
<span className={labelTextClass}>{props.labelId}</span>
|
||||
</label>
|
||||
<Slider
|
||||
data-cy="label-group-item"
|
||||
data-label-type="slider"
|
||||
aria-roledescription="slider"
|
||||
defaultValue={0}
|
||||
isDisabled={!props.isEditable}
|
||||
onChangeEnd={(val) => props.sliderHandler(val / 100)}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb bg="gainsboro" />
|
||||
</Slider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Flex, IconButton, Tooltip, useColorModeValue } from "@chakra-ui/react";
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import { Edit2 } from "lucide-react";
|
||||
import { SkipButton } from "src/components/Buttons/Skip";
|
||||
import { SubmitButton } from "src/components/Buttons/Submit";
|
||||
import { TaskInfo } from "src/components/TaskInfo/TaskInfo";
|
||||
@@ -36,12 +36,18 @@ export const TaskControls = (props: TaskControlsProps) => {
|
||||
{props.taskStatus === "REVIEW" || props.taskStatus === "SUBMITTED" ? (
|
||||
<>
|
||||
<Tooltip label="Edit">
|
||||
<IconButton size="lg" data-cy="edit" aria-label="edit" onClick={props.onEdit} icon={<FiEdit2 />} />
|
||||
<IconButton
|
||||
size="lg"
|
||||
data-cy="edit"
|
||||
aria-label="edit"
|
||||
onClick={props.onEdit}
|
||||
icon={<Edit2 size="1em" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SubmitButton
|
||||
colorScheme="green"
|
||||
data-cy="submit"
|
||||
disabled={props.taskStatus === "SUBMITTED"}
|
||||
isDisabled={props.taskStatus === "SUBMITTED"}
|
||||
onClick={props.onSubmit}
|
||||
>
|
||||
Submit
|
||||
@@ -53,7 +59,7 @@ export const TaskControls = (props: TaskControlsProps) => {
|
||||
<SubmitButton
|
||||
colorScheme="blue"
|
||||
data-cy="review"
|
||||
disabled={props.taskStatus === "NOT_SUBMITTABLE"}
|
||||
isDisabled={props.taskStatus === "NOT_SUBMITTABLE"}
|
||||
onClick={props.onReview}
|
||||
>
|
||||
Review
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { TaskEmptyState } from "src/components/EmptyState";
|
||||
import { LoadingScreen } from "src/components/Loading/LoadingScreen";
|
||||
import { Task } from "src/components/Tasks/Task";
|
||||
import { TaskInfos } from "src/components/Tasks/TaskTypes";
|
||||
import { ERROR_CODES, taskApiHooks } from "src/lib/constants";
|
||||
import { getTypeSafei18nKey } from "src/lib/i18n";
|
||||
import { TaskType } from "src/types/Task";
|
||||
|
||||
type TaskPageProps = {
|
||||
type: TaskType;
|
||||
};
|
||||
|
||||
export const TaskPage = ({ type }: TaskPageProps) => {
|
||||
const { t } = useTranslation(["tasks", "common"]);
|
||||
const taskApiHook = taskApiHooks[type];
|
||||
const { tasks, isLoading, reset, trigger, error } = taskApiHook(type);
|
||||
const taskInfo = TaskInfos.find((taskType) => taskType.type === type);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen text={t("common:loading")} />;
|
||||
}
|
||||
|
||||
if (tasks.length === 0 || error?.errorCode === ERROR_CODES.TASK_REQUESTED_TYPE_NOT_AVAILABLE) {
|
||||
return <TaskEmptyState />;
|
||||
}
|
||||
|
||||
const task = tasks[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t(getTypeSafei18nKey(`${taskInfo.id}.label`))}</title>
|
||||
<meta name="description" content={t(getTypeSafei18nKey(`${taskInfo.id}.desc`))} />
|
||||
</Head>
|
||||
<Task key={task.task.id} frontendId={task.id} task={task.task} trigger={trigger} mutate={reset} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
|
||||
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||
import { TaskHeader } from "src/components/Tasks/TaskHeader";
|
||||
import { getTypeSafei18nKey } from "src/lib/i18n";
|
||||
|
||||
export const CreateTask = ({
|
||||
task,
|
||||
@@ -12,19 +14,22 @@ export const CreateTask = ({
|
||||
isEditable,
|
||||
isDisabled,
|
||||
onReplyChanged,
|
||||
onValidityChanged,
|
||||
}: TaskSurveyProps<{ text: string }>) => {
|
||||
const { t, i18n } = useTranslation(["tasks", "common"]);
|
||||
const cardColor = useColorModeValue("gray.50", "gray.800");
|
||||
const titleColor = useColorModeValue("gray.800", "gray.300");
|
||||
|
||||
const [inputText, setInputText] = useState("");
|
||||
|
||||
const textChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const text = event.target.value;
|
||||
const isTextBlank = !text || /^\s*$/.test(text) ? true : false;
|
||||
onReplyChanged({ text });
|
||||
const isTextBlank = !text || /^\s*$/.test(text);
|
||||
if (!isTextBlank) {
|
||||
onReplyChanged({ content: { text }, state: "VALID" });
|
||||
onValidityChanged("VALID");
|
||||
setInputText(text);
|
||||
} else {
|
||||
onReplyChanged({ content: { text }, state: "INVALID" });
|
||||
onValidityChanged("INVALID");
|
||||
setInputText("");
|
||||
}
|
||||
};
|
||||
@@ -34,22 +39,24 @@ export const CreateTask = ({
|
||||
<TwoColumnsWithCards>
|
||||
<>
|
||||
<TaskHeader taskType={taskType} />
|
||||
{task.conversation ? (
|
||||
{!!task.conversation && (
|
||||
<Box mt="4" borderRadius="lg" bg={cardColor} className="p-3 sm:p-6">
|
||||
<MessageTable messages={task.conversation.messages} />
|
||||
<MessageTable messages={task.conversation.messages} highlightLastMessage />
|
||||
</Box>
|
||||
) : null}
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
<Stack spacing="4">
|
||||
<Text fontSize="xl" fontWeight="bold" color={titleColor}>
|
||||
{taskType.instruction}
|
||||
</Text>
|
||||
{!!i18n.exists(`task.${taskType.id}.instruction`) && (
|
||||
<Text fontSize="xl" fontWeight="bold" color={titleColor}>
|
||||
{t(getTypeSafei18nKey(`${taskType.id}.instruction`))}
|
||||
</Text>
|
||||
)}
|
||||
<TrackedTextarea
|
||||
text={inputText}
|
||||
onTextChange={textChangeHandler}
|
||||
thresholds={{ low: 20, medium: 40, goal: 50 }}
|
||||
textareaProps={{ placeholder: "Write your prompt here...", isDisabled, isReadOnly: !isEditable }}
|
||||
textareaProps={{ placeholder: t("tasks:write_initial_prompt"), isDisabled, isReadOnly: !isEditable }}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { Sortable } from "src/components/Sortable/Sortable";
|
||||
import { SurveyCard } from "src/components/Survey/SurveyCard";
|
||||
@@ -12,23 +12,26 @@ export const EvaluateTask = ({
|
||||
isEditable,
|
||||
isDisabled,
|
||||
onReplyChanged,
|
||||
onValidityChanged,
|
||||
}: TaskSurveyProps<{ ranking: number[] }>) => {
|
||||
const cardColor = useColorModeValue("gray.50", "gray.800");
|
||||
const [ranking, setRanking] = useState<number[]>(null);
|
||||
|
||||
let messages = [];
|
||||
if (task.conversation) {
|
||||
messages = task.conversation.messages;
|
||||
messages = messages.map((message, index) => ({ ...message, id: index }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const ranking = (task.replies ?? task.prompts).map((_, idx) => idx);
|
||||
onReplyChanged({ content: { ranking }, state: "DEFAULT" });
|
||||
}, [task, onReplyChanged]);
|
||||
|
||||
const onRank = (newRanking: number[]) => {
|
||||
onReplyChanged({ content: { ranking: newRanking }, state: "VALID" });
|
||||
};
|
||||
if (ranking === null) {
|
||||
const defaultRanking = (task.replies ?? task.prompts).map((_, idx) => idx);
|
||||
onReplyChanged({ ranking: defaultRanking });
|
||||
onValidityChanged("DEFAULT");
|
||||
} else {
|
||||
onReplyChanged({ ranking });
|
||||
onValidityChanged("VALID");
|
||||
}
|
||||
}, [task, ranking, onReplyChanged, onValidityChanged]);
|
||||
|
||||
const sortables = task.replies ? "replies" : "prompts";
|
||||
|
||||
@@ -38,13 +41,13 @@ export const EvaluateTask = ({
|
||||
<SurveyCard>
|
||||
<TaskHeader taskType={taskType} />
|
||||
<Box mt="4" p="6" borderRadius="lg" bg={cardColor}>
|
||||
<MessageTable messages={messages} />
|
||||
<MessageTable messages={messages} highlightLastMessage />
|
||||
</Box>
|
||||
<Sortable
|
||||
items={task[sortables]}
|
||||
isDisabled={isDisabled}
|
||||
isEditable={isEditable}
|
||||
onChange={onRank}
|
||||
onChange={setRanking}
|
||||
className="my-8"
|
||||
/>
|
||||
</SurveyCard>
|
||||
|
||||
@@ -1,72 +1,94 @@
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { Box, Button, Flex, HStack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MessageView } from "src/components/Messages";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { LabelRadioGroup } from "src/components/Survey/LabelRadioGroup";
|
||||
import { LabelSliderGroup } from "src/components/Survey/LabelSliderGroup";
|
||||
import { LabelInputGroup } from "src/components/Survey/LabelInputGroup";
|
||||
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||
import { TaskHeader } from "src/components/Tasks/TaskHeader";
|
||||
import { TaskType } from "src/types/Task";
|
||||
|
||||
export const LabelTask = ({
|
||||
task,
|
||||
taskType,
|
||||
onReplyChanged,
|
||||
isEditable,
|
||||
onReplyChanged,
|
||||
onValidityChanged,
|
||||
}: TaskSurveyProps<{ text: string; labels: Record<string, number>; message_id: string }>) => {
|
||||
const valid_labels = task.valid_labels;
|
||||
const [sliderValues, setSliderValues] = useState<number[]>(new Array(valid_labels.length).fill(0));
|
||||
const [sliderValues, setSliderValues] = useState<number[]>(new Array(task.valid_labels.length).fill(null));
|
||||
|
||||
useEffect(() => {
|
||||
onReplyChanged({
|
||||
content: { labels: {}, text: task.reply, message_id: task.message_id },
|
||||
state: "NOT_SUBMITTABLE",
|
||||
});
|
||||
}, [task, onReplyChanged]);
|
||||
|
||||
const onSliderChange = (values: number[]) => {
|
||||
console.assert(valid_labels.length === sliderValues.length);
|
||||
const labels = Object.fromEntries(valid_labels.map((label, i) => [label, sliderValues[i]]));
|
||||
onReplyChanged({
|
||||
content: { labels, text: task.reply || task.prompt, message_id: task.message_id },
|
||||
state: "VALID",
|
||||
});
|
||||
setSliderValues(values);
|
||||
};
|
||||
console.assert(task.valid_labels.length === sliderValues.length);
|
||||
const labels = Object.fromEntries(task.valid_labels.map((label, i) => [label, sliderValues[i]]));
|
||||
onReplyChanged({ labels, text: task.reply || task.prompt, message_id: task.message_id });
|
||||
onValidityChanged(sliderValues.every((value) => value !== null) ? "VALID" : "INVALID");
|
||||
}, [task, sliderValues, onReplyChanged, onValidityChanged]);
|
||||
|
||||
const cardColor = useColorModeValue("gray.50", "gray.800");
|
||||
const isSpamTask = task.mode === "simple" && task.valid_labels.length === 1 && task.valid_labels[0] === "spam";
|
||||
|
||||
return (
|
||||
<div data-cy="task" data-task-type="label-task">
|
||||
<div data-cy="task" data-task-type={isSpamTask ? "spam-task" : "label-task"}>
|
||||
<TwoColumnsWithCards>
|
||||
<>
|
||||
<TaskHeader taskType={taskType} />
|
||||
{task.conversation ? (
|
||||
<Box mt="4" p="6" borderRadius="lg" bg={cardColor}>
|
||||
<Box mt="4" p={[4, 6]} borderRadius="lg" bg={cardColor}>
|
||||
<MessageTable
|
||||
messages={[
|
||||
...(task.conversation?.messages ?? []),
|
||||
{
|
||||
text: task.reply,
|
||||
is_assistant: task.type === TaskType.label_assistant_reply,
|
||||
message_id: task.message_id,
|
||||
},
|
||||
]}
|
||||
messages={[...(task.conversation?.messages ?? []), task.reply_message]}
|
||||
highlightLastMessage
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box mt="4">
|
||||
<MessageView text={task.prompt} is_assistant={false} id={task.message_id} />
|
||||
<MessageView text={task.prompt} is_assistant={false} id={task.message_id} emojis={{}} user_emojis={[]} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
{task.mode === "simple" ? (
|
||||
<LabelRadioGroup labelIDs={task.valid_labels} isEditable={isEditable} onChange={onSliderChange} />
|
||||
{isSpamTask ? (
|
||||
<SpamTaskInput
|
||||
value={sliderValues[0]}
|
||||
onChange={(value) => setSliderValues([value])}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
) : (
|
||||
<LabelSliderGroup labelIDs={task.valid_labels} isEditable={isEditable} onChange={onSliderChange} />
|
||||
<Flex direction="column" alignItems="stretch">
|
||||
<Text>The highlighted message:</Text>
|
||||
<LabelInputGroup labelIDs={task.valid_labels} isEditable={isEditable} onChange={setSliderValues} />
|
||||
</Flex>
|
||||
)}
|
||||
</TwoColumnsWithCards>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SpamTaskInput = ({
|
||||
isEditable,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
isEditable: boolean;
|
||||
value: number;
|
||||
onChange: (number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<HStack>
|
||||
<Text>Is the highlighted message spam?</Text>
|
||||
<Button
|
||||
data-cy="spam-button"
|
||||
isDisabled={!isEditable}
|
||||
colorScheme={value === 1 ? "blue" : undefined}
|
||||
onClick={() => onChange(1)}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
<Button
|
||||
data-cy="not-spam-button"
|
||||
isDisabled={!isEditable}
|
||||
colorScheme={value === 0 ? "blue" : undefined}
|
||||
onClick={() => onChange(0)}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import { TaskControls } from "src/components/Survey/TaskControls";
|
||||
import { CreateTask } from "src/components/Tasks/CreateTask";
|
||||
import { EvaluateTask } from "src/components/Tasks/EvaluateTask";
|
||||
import { LabelTask } from "src/components/Tasks/LabelTask";
|
||||
import { TaskCategory, TaskInfo, TaskTypes } from "src/components/Tasks/TaskTypes";
|
||||
import { TaskCategory, TaskInfo, TaskInfos } from "src/components/Tasks/TaskTypes";
|
||||
import { UnchangedWarning } from "src/components/Tasks/UnchangedWarning";
|
||||
import { post } from "src/lib/api";
|
||||
import { TaskContent } from "src/types/Task";
|
||||
import { TaskReplyState } from "src/types/TaskReplyState";
|
||||
import { getTypeSafei18nKey } from "src/lib/i18n";
|
||||
import { TaskContent, TaskReplyValidity } from "src/types/Task";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
export type TaskStatus = "NOT_SUBMITTABLE" | "DEFAULT" | "VALID" | "REVIEW" | "SUBMITTED";
|
||||
@@ -19,17 +20,19 @@ export interface TaskSurveyProps<T> {
|
||||
taskType: TaskInfo;
|
||||
isEditable: boolean;
|
||||
isDisabled?: boolean;
|
||||
onReplyChanged: (state: TaskReplyState<T>) => void;
|
||||
onReplyChanged: (content: T) => void;
|
||||
onValidityChanged: (validity: TaskReplyValidity) => void;
|
||||
}
|
||||
|
||||
export const Task = ({ frontendId, task, trigger, mutate }) => {
|
||||
const { t } = useTranslation("tasks");
|
||||
const [taskStatus, setTaskStatus] = useState<TaskStatus>("NOT_SUBMITTABLE");
|
||||
const replyContent = useRef<TaskContent>(null);
|
||||
const [showUnchangedWarning, setShowUnchangedWarning] = useState(false);
|
||||
|
||||
const rootEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
const taskType = TaskTypes.find((taskType) => taskType.type === task.type && taskType.mode === task.mode);
|
||||
const taskType = TaskInfos.find((taskType) => taskType.type === task.type && taskType.mode === task.mode);
|
||||
|
||||
const { trigger: sendRejection } = useSWRMutation("/api/reject_task", post, {
|
||||
onSuccess: async () => {
|
||||
@@ -44,20 +47,27 @@ export const Task = ({ frontendId, task, trigger, mutate }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onReplyChanged = useRef((state: TaskReplyState<TaskContent>) => {
|
||||
if (taskStatus === "SUBMITTED") return;
|
||||
const edit_mode = taskStatus === "NOT_SUBMITTABLE" || taskStatus === "DEFAULT" || taskStatus === "VALID";
|
||||
const submitted = taskStatus === "SUBMITTED";
|
||||
|
||||
replyContent.current = state?.content;
|
||||
if (state === null) {
|
||||
if (taskStatus !== "NOT_SUBMITTABLE") setTaskStatus("NOT_SUBMITTABLE");
|
||||
} else if (state.state === "DEFAULT") {
|
||||
if (taskStatus !== "DEFAULT") setTaskStatus("DEFAULT");
|
||||
} else if (state.state === "VALID") {
|
||||
if (taskStatus !== "VALID") setTaskStatus("VALID");
|
||||
} else if (state.state === "INVALID") {
|
||||
setTaskStatus("NOT_SUBMITTABLE");
|
||||
const onValidityChanged = (validity: TaskReplyValidity) => {
|
||||
if (!edit_mode) return;
|
||||
switch (validity) {
|
||||
case "DEFAULT":
|
||||
if (taskStatus !== "DEFAULT") setTaskStatus("DEFAULT");
|
||||
break;
|
||||
case "VALID":
|
||||
if (taskStatus !== "VALID") setTaskStatus("VALID");
|
||||
break;
|
||||
case "INVALID":
|
||||
if (taskStatus !== "NOT_SUBMITTABLE") setTaskStatus("NOT_SUBMITTABLE");
|
||||
break;
|
||||
}
|
||||
}).current;
|
||||
};
|
||||
|
||||
const onReplyChanged = (content: TaskContent) => {
|
||||
replyContent.current = content;
|
||||
};
|
||||
|
||||
const reviewResponse = () => {
|
||||
switch (taskStatus) {
|
||||
@@ -99,42 +109,39 @@ export const Task = ({ frontendId, task, trigger, mutate }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const edit_mode = taskStatus === "NOT_SUBMITTABLE" || taskStatus === "DEFAULT" || taskStatus === "VALID";
|
||||
const submitted = taskStatus === "SUBMITTED";
|
||||
|
||||
function taskTypeComponent() {
|
||||
switch (taskType.category) {
|
||||
case TaskCategory.Create:
|
||||
return (
|
||||
<CreateTask
|
||||
key={task.id}
|
||||
task={task}
|
||||
taskType={taskType}
|
||||
isEditable={edit_mode}
|
||||
isDisabled={submitted}
|
||||
onReplyChanged={onReplyChanged}
|
||||
onValidityChanged={onValidityChanged}
|
||||
/>
|
||||
);
|
||||
case TaskCategory.Evaluate:
|
||||
return (
|
||||
<EvaluateTask
|
||||
key={task.id}
|
||||
task={task}
|
||||
taskType={taskType}
|
||||
isEditable={edit_mode}
|
||||
isDisabled={submitted}
|
||||
onReplyChanged={onReplyChanged}
|
||||
onValidityChanged={onValidityChanged}
|
||||
/>
|
||||
);
|
||||
case TaskCategory.Label:
|
||||
return (
|
||||
<LabelTask
|
||||
key={task.id}
|
||||
task={task}
|
||||
taskType={taskType}
|
||||
isEditable={edit_mode}
|
||||
isDisabled={submitted}
|
||||
onReplyChanged={onReplyChanged}
|
||||
onValidityChanged={onValidityChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -153,8 +160,8 @@ export const Task = ({ frontendId, task, trigger, mutate }) => {
|
||||
/>
|
||||
<UnchangedWarning
|
||||
show={showUnchangedWarning}
|
||||
title={taskType.unchanged_title || "No changes"}
|
||||
message={taskType.unchanged_message || "Are you sure you would like to continue?"}
|
||||
title={t(getTypeSafei18nKey(`${taskType.id}.unchanged_title`)) || t("default.unchanged_title")}
|
||||
message={t(getTypeSafei18nKey(`${taskType.id}.unchanged_message`)) || t("default.unchanged_message")}
|
||||
continueButtonText={"Continue anyway"}
|
||||
onClose={() => setShowUnchangedWarning(false)}
|
||||
onContinueAnyway={() => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { HStack, IconButton, Link, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { FiHelpCircle } from "react-icons/fi";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import type { TaskInfo } from "src/components/Tasks/TaskTypes";
|
||||
import { getTypeSafei18nKey } from "src/lib/i18n";
|
||||
|
||||
interface TaskHeaderProps {
|
||||
/**
|
||||
@@ -13,20 +15,21 @@ interface TaskHeaderProps {
|
||||
* Presents the Task label, instructions, and help link
|
||||
*/
|
||||
const TaskHeader = ({ taskType }: TaskHeaderProps) => {
|
||||
const { t } = useTranslation(["tasks", "common"]);
|
||||
const labelColor = useColorModeValue("gray.600", "gray.400");
|
||||
const titleColor = useColorModeValue("gray.800", "gray.300");
|
||||
return (
|
||||
<Stack spacing="1">
|
||||
<HStack>
|
||||
<Text fontSize="xl" fontWeight="bold" color={titleColor}>
|
||||
{taskType.label}
|
||||
{t(getTypeSafei18nKey(`${taskType.id}.label`))}
|
||||
</Text>
|
||||
<Link href={taskType.help_link} isExternal>
|
||||
<IconButton variant="ghost" aria-label="More Information" icon={<FiHelpCircle />} />
|
||||
<IconButton variant="ghost" aria-label="More Information" icon={<HelpCircle size="1em" />} />
|
||||
</Link>
|
||||
</HStack>
|
||||
<Text fontSize="md" color={labelColor}>
|
||||
{taskType.overview}
|
||||
{t(getTypeSafei18nKey(`${taskType.id}.overview`))}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,181 +1,150 @@
|
||||
import { TaskType } from "src/types/Task";
|
||||
|
||||
export enum TaskCategory {
|
||||
Random = "Random",
|
||||
Create = "Create",
|
||||
Evaluate = "Evaluate",
|
||||
Label = "Label",
|
||||
Random = "Random",
|
||||
}
|
||||
|
||||
export enum TaskUpdateType {
|
||||
MessageRanking = "message_ranking",
|
||||
Random = "random",
|
||||
TextLabels = "text_labels",
|
||||
TextReplyToMessage = "text_reply_to_message",
|
||||
}
|
||||
|
||||
export interface TaskInfo {
|
||||
label: string;
|
||||
desc: string;
|
||||
category: TaskCategory;
|
||||
help_link: string;
|
||||
id: string;
|
||||
mode?: string;
|
||||
pathname: string;
|
||||
type: string;
|
||||
help_link: string;
|
||||
mode?: string;
|
||||
overview?: string;
|
||||
instruction?: string;
|
||||
update_type: string;
|
||||
unchanged_title?: string;
|
||||
unchanged_message?: string;
|
||||
}
|
||||
|
||||
export const TaskCategoryLabels: { [key in TaskCategory]: string } = {
|
||||
[TaskCategory.Random]: "I'm feeling lucky",
|
||||
[TaskCategory.Create]: "Create",
|
||||
[TaskCategory.Evaluate]: "Evaluate",
|
||||
[TaskCategory.Label]: "Label",
|
||||
[TaskCategory.Random]: "grab_a_task",
|
||||
[TaskCategory.Create]: "create",
|
||||
[TaskCategory.Evaluate]: "evaluate",
|
||||
[TaskCategory.Label]: "label",
|
||||
};
|
||||
|
||||
export const TaskTypes: TaskInfo[] = [
|
||||
export const TaskInfos: TaskInfo[] = [
|
||||
// general/random
|
||||
{
|
||||
label: "Start a Task",
|
||||
desc: "Help us improve Open Assistant by starting a random task.",
|
||||
id: "random",
|
||||
category: TaskCategory.Random,
|
||||
pathname: "/tasks/random",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
type: "random",
|
||||
update_type: "random",
|
||||
type: TaskType.random,
|
||||
update_type: TaskUpdateType.Random,
|
||||
},
|
||||
// create
|
||||
{
|
||||
label: "Create Initial Prompts",
|
||||
desc: "Write initial prompts to help Open Assistant to try replying to diverse messages.",
|
||||
id: "create_initial_prompt",
|
||||
category: TaskCategory.Create,
|
||||
pathname: "/create/initial_prompt",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
type: "initial_prompt",
|
||||
overview: "Create an initial message to send to the assistant",
|
||||
instruction: "Provide the initial prompt",
|
||||
update_type: "text_reply_to_message",
|
||||
type: TaskType.initial_prompt,
|
||||
update_type: TaskUpdateType.TextReplyToMessage,
|
||||
},
|
||||
{
|
||||
label: "Reply as User",
|
||||
desc: "Chat with Open Assistant and help improve it’s responses as you interact with it.",
|
||||
id: "reply_as_user",
|
||||
category: TaskCategory.Create,
|
||||
pathname: "/create/user_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/tasks/reply_as_user",
|
||||
type: "prompter_reply",
|
||||
overview: "Given the following conversation, provide an adequate reply",
|
||||
instruction: "Provide the user's reply",
|
||||
update_type: "text_reply_to_message",
|
||||
type: TaskType.prompter_reply,
|
||||
update_type: TaskUpdateType.TextReplyToMessage,
|
||||
},
|
||||
{
|
||||
label: "Reply as Assistant",
|
||||
desc: "Help Open Assistant improve its responses to conversations with other users.",
|
||||
id: "reply_as_assistant",
|
||||
category: TaskCategory.Create,
|
||||
pathname: "/create/assistant_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/tasks/reply_as_assistant",
|
||||
type: "assistant_reply",
|
||||
overview: "Given the following conversation, provide an adequate reply",
|
||||
instruction: "Provide the assistant's reply",
|
||||
update_type: "text_reply_to_message",
|
||||
type: TaskType.assistant_reply,
|
||||
update_type: TaskUpdateType.TextReplyToMessage,
|
||||
},
|
||||
// evaluate
|
||||
{
|
||||
label: "Rank User Replies",
|
||||
id: "rank_user_replies",
|
||||
category: TaskCategory.Evaluate,
|
||||
desc: "Help Open Assistant improve its responses to conversations with other users.",
|
||||
pathname: "/evaluate/rank_user_replies",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Given the following User replies, sort them from best to worst, best being first, worst being last.",
|
||||
type: "rank_prompter_replies",
|
||||
update_type: "message_ranking",
|
||||
unchanged_title: "Order Unchanged",
|
||||
unchanged_message: "You have not changed the order of the prompts. Are you sure you would like to continue?",
|
||||
type: TaskType.rank_prompter_replies,
|
||||
update_type: TaskUpdateType.MessageRanking,
|
||||
},
|
||||
{
|
||||
label: "Rank Assistant Replies",
|
||||
desc: "Score prompts given by Open Assistant based on their accuracy and readability.",
|
||||
id: "rank_assistant_replies",
|
||||
category: TaskCategory.Evaluate,
|
||||
pathname: "/evaluate/rank_assistant_replies",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview:
|
||||
"Given the following Assistant replies, sort them from best to worst, best being first, worst being last.",
|
||||
type: "rank_assistant_replies",
|
||||
update_type: "message_ranking",
|
||||
unchanged_title: "Order Unchanged",
|
||||
unchanged_message: "You have not changed the order of the prompts. Are you sure you would like to continue?",
|
||||
type: TaskType.rank_assistant_replies,
|
||||
update_type: TaskUpdateType.MessageRanking,
|
||||
},
|
||||
{
|
||||
label: "Rank Initial Prompts",
|
||||
desc: "Score prompts given by Open Assistant based on their accuracy and readability.",
|
||||
id: "rank_initial_prompts",
|
||||
category: TaskCategory.Evaluate,
|
||||
pathname: "/evaluate/rank_initial_prompts",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Given the following inital prompts, sort them from best to worst, best being first, worst being last.",
|
||||
type: "rank_initial_prompts",
|
||||
update_type: "message_ranking",
|
||||
unchanged_title: "Order Unchanged",
|
||||
unchanged_message: "You have not changed the order of the prompts. Are you sure you would like to continue?",
|
||||
type: TaskType.rank_initial_prompts,
|
||||
update_type: TaskUpdateType.MessageRanking,
|
||||
},
|
||||
// label (full)
|
||||
{
|
||||
label: "Label Initial Prompt",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "label_initial_prompt",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_initial_prompt",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Provide labels for the following prompt",
|
||||
type: "label_initial_prompt",
|
||||
type: TaskType.label_initial_prompt,
|
||||
mode: "full",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
{
|
||||
label: "Label Prompter Reply",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "label_prompter_reply",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_prompter_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/tasks/label_prompter_reply",
|
||||
overview: "Given the following discussion, provide labels for the final prompt.",
|
||||
type: "label_prompter_reply",
|
||||
type: TaskType.label_prompter_reply,
|
||||
mode: "full",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
{
|
||||
label: "Label Assistant Reply",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "label_assistant_reply",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_assistant_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/tasks/label_assistant_reply",
|
||||
overview: "Given the following discussion, provide labels for the final prompt.",
|
||||
type: "label_assistant_reply",
|
||||
type: TaskType.label_assistant_reply,
|
||||
mode: "full",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
// label (simple)
|
||||
{
|
||||
label: "Classify Initial Prompt",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "classify_initial_prompt",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_initial_prompt",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Read the following prompt and then answer the question about it.",
|
||||
type: "label_initial_prompt",
|
||||
type: TaskType.label_initial_prompt,
|
||||
mode: "simple",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
{
|
||||
label: "Classify Prompter Reply",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "classify_prompter_reply",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_prompter_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Read the following conversation and then answer the question about the last prompt in the discussion.",
|
||||
type: "label_prompter_reply",
|
||||
type: TaskType.label_prompter_reply,
|
||||
mode: "simple",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
{
|
||||
label: "Classify Assistant Reply",
|
||||
desc: "Provide labels for a prompt.",
|
||||
id: "classify_assistant_reply",
|
||||
category: TaskCategory.Label,
|
||||
pathname: "/label/label_assistant_reply",
|
||||
help_link: "https://projects.laion.ai/Open-Assistant/docs/guides/prompting",
|
||||
overview: "Read the following conversation and then answer the question about the last prompt in the discussion.",
|
||||
type: "label_assistant_reply",
|
||||
type: TaskType.label_assistant_reply,
|
||||
mode: "simple",
|
||||
update_type: "text_labels",
|
||||
update_type: TaskUpdateType.TextLabels,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Card, CardBody, IconButton } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Pencil } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { memo, useState } from "react";
|
||||
import { get } from "src/lib/api";
|
||||
import type { FetchUsersResponse, User } from "src/types/Users";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable";
|
||||
|
||||
interface Pagination {
|
||||
/**
|
||||
* The user's `display_name` used for pagination.
|
||||
*/
|
||||
cursor: string;
|
||||
|
||||
/**
|
||||
* The pagination direction.
|
||||
*/
|
||||
direction: "forward" | "back";
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
const columns: DataTableColumnDef<User>[] = [
|
||||
columnHelper.accessor("user_id", {
|
||||
header: "ID",
|
||||
}),
|
||||
columnHelper.accessor("id", {
|
||||
header: "Auth ID",
|
||||
}),
|
||||
columnHelper.accessor("auth_method", {
|
||||
header: "Auth Method",
|
||||
}),
|
||||
{
|
||||
...columnHelper.accessor("display_name", {
|
||||
header: "Name",
|
||||
}),
|
||||
filterable: true,
|
||||
},
|
||||
columnHelper.accessor("role", {
|
||||
header: "Role",
|
||||
}),
|
||||
columnHelper.accessor((user) => user.user_id, {
|
||||
cell: ({ getValue }) => (
|
||||
<IconButton
|
||||
as={Link}
|
||||
href={`/admin/manage_user/${getValue()}`}
|
||||
aria-label="Manage"
|
||||
icon={<Pencil size="1em"></Pencil>}
|
||||
></IconButton>
|
||||
),
|
||||
header: "Update",
|
||||
}),
|
||||
];
|
||||
|
||||
export const UserTable = memo(function UserTable() {
|
||||
const [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" });
|
||||
const [filterValues, setFilterValues] = useState<FilterItem[]>([]);
|
||||
const handleFilterValuesChange = (values: FilterItem[]) => {
|
||||
setFilterValues(values);
|
||||
setPagination((old) => ({ ...old, cursor: "" }));
|
||||
};
|
||||
// Fetch and save the users.
|
||||
// This follows useSWR's recommendation for simple pagination:
|
||||
// https://swr.vercel.app/docs/pagination#when-to-use-useswr
|
||||
const display_name = filterValues.find((value) => value.id === "display_name")?.value ?? "";
|
||||
const { data, error } = useSWR<FetchUsersResponse<User>>(
|
||||
`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}&searchDisplayName=${display_name}&sortKey=display_name`,
|
||||
get,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
const toPreviousPage = () => {
|
||||
setPagination({
|
||||
cursor: data.prev,
|
||||
direction: "back",
|
||||
});
|
||||
};
|
||||
|
||||
const toNextPage = () => {
|
||||
setPagination({
|
||||
cursor: data.next,
|
||||
direction: "forward",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<DataTable
|
||||
data={data?.items || []}
|
||||
columns={columns}
|
||||
caption="Users"
|
||||
onNextClick={toNextPage}
|
||||
onPreviousClick={toPreviousPage}
|
||||
disableNext={!data?.next}
|
||||
disablePrevious={!data?.prev}
|
||||
filterValues={filterValues}
|
||||
onFilterChange={handleFilterValuesChange}
|
||||
></DataTable>
|
||||
{error && "Unable to load users."}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Spacer,
|
||||
Stack,
|
||||
Table,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { get } from "src/lib/api";
|
||||
import type { User } from "src/types/Users";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface Pagination {
|
||||
/**
|
||||
* The user's `display_name` used for pagination.
|
||||
*/
|
||||
cursor: string;
|
||||
|
||||
/**
|
||||
* The pagination direction.
|
||||
*/
|
||||
direction: "forward" | "back";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches users from the users api route and then presents them in a simple Chakra table.
|
||||
*/
|
||||
const UsersCell = () => {
|
||||
const toast = useToast();
|
||||
const [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" });
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
// Fetch and save the users.
|
||||
// This follows useSWR's recommendation for simple pagination:
|
||||
// https://swr.vercel.app/docs/pagination#when-to-use-useswr
|
||||
useSWR(`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}`, get, {
|
||||
onSuccess: (data) => {
|
||||
// When no more users can be found, trigger a toast to indicate why no
|
||||
// changes have taken place. We have to maintain a non-empty set of
|
||||
// users otherwise we can't paginate using a cursor (since we've lost the
|
||||
// cursor).
|
||||
if (data.length === 0) {
|
||||
toast({
|
||||
title: "No more users",
|
||||
status: "warning",
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setUsers(data);
|
||||
},
|
||||
});
|
||||
|
||||
const toPreviousPage = () => {
|
||||
if (users.length >= 0) {
|
||||
setPagination({
|
||||
cursor: users[0].display_name,
|
||||
direction: "back",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Can not paginate when no users are found",
|
||||
status: "warning",
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toNextPage = () => {
|
||||
if (users.length >= 0) {
|
||||
setPagination({
|
||||
cursor: users[users.length - 1].display_name,
|
||||
direction: "forward",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Can not paginate when no users are found",
|
||||
status: "warning",
|
||||
duration: 1000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Present users in a naive table.
|
||||
return (
|
||||
<Stack>
|
||||
<Flex p="2">
|
||||
<Button onClick={toPreviousPage}>Previous</Button>
|
||||
<Spacer />
|
||||
<Button onClick={toNextPage}>Next</Button>
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table variant="simple">
|
||||
<TableCaption>Users</TableCaption>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Id</Th>
|
||||
<Th>Auth Id</Th>
|
||||
<Th>Auth Method</Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Update</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{users.map(({ id, user_id, auth_method, display_name, role }) => (
|
||||
<Tr key={user_id}>
|
||||
<Td>{user_id}</Td>
|
||||
<Td>{id}</Td>
|
||||
<Td>{auth_method}</Td>
|
||||
<Td>{display_name}</Td>
|
||||
<Td>{role}</Td>
|
||||
<Td>
|
||||
<Link href={`/admin/manage_user/${user_id}`}>Manage</Link>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersCell;
|
||||
@@ -20,7 +20,8 @@ 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);
|
||||
const err = error?.response?.data;
|
||||
throw new OasstError(err?.message ?? error, err?.errorCode, error?.response?.httpStatusCode || -1);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ const withoutRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiRe
|
||||
* Wraps any API Route handler and verifies that the user has the appropriate
|
||||
* role before running the handler. Returns a 403 otherwise.
|
||||
*/
|
||||
const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse) => void) => {
|
||||
const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse, token: JWT) => void) => {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const token = await getToken({ req });
|
||||
if (!token || token.role !== role) {
|
||||
res.status(403).end();
|
||||
return;
|
||||
}
|
||||
return handler(req, res);
|
||||
return handler(req, res, token);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
useCreateAssistantReply,
|
||||
useCreateInitialPrompt,
|
||||
useCreatePrompterReply,
|
||||
} from "src/hooks/tasks/useCreateReply";
|
||||
import { useGenericTaskAPI } from "src/hooks/tasks/useGenericTaskAPI";
|
||||
import {
|
||||
useLabelAssistantReplyTask,
|
||||
useLabelInitialPromptTask,
|
||||
useLabelPrompterReplyTask,
|
||||
} from "src/hooks/tasks/useLabelingTask";
|
||||
import {
|
||||
useRankAssistantRepliesTask,
|
||||
useRankInitialPromptsTask,
|
||||
useRankPrompterRepliesTask,
|
||||
} from "src/hooks/tasks/useRankReplies";
|
||||
import { TaskApiHooks } from "src/types/Hooks";
|
||||
import { TaskType } from "src/types/Task";
|
||||
|
||||
export const ERROR_CODES = {
|
||||
TASK_REQUESTED_TYPE_NOT_AVAILABLE: 1006,
|
||||
TASK_INVALID_REQUEST_TYPE: 1000,
|
||||
TASK_ACK_FAILED: 1001,
|
||||
TASK_NACK_FAILED: 1002,
|
||||
TASK_INVALID_RESPONSE_TYPE: 1003,
|
||||
TASK_INTERACTION_REQUEST_FAILED: 1004,
|
||||
TASK_GENERATION_FAILED: 1005,
|
||||
TASK_AVAILABILITY_QUERY_FAILED: 1007,
|
||||
TASK_MESSAGE_TOO_LONG: 1008,
|
||||
};
|
||||
|
||||
export const taskApiHooks: TaskApiHooks = {
|
||||
[TaskType.random]: useGenericTaskAPI,
|
||||
[TaskType.assistant_reply]: useCreateAssistantReply,
|
||||
[TaskType.initial_prompt]: useCreateInitialPrompt,
|
||||
[TaskType.label_assistant_reply]: useLabelAssistantReplyTask,
|
||||
[TaskType.label_initial_prompt]: useLabelInitialPromptTask,
|
||||
[TaskType.label_prompter_reply]: useLabelPrompterReplyTask,
|
||||
[TaskType.prompter_reply]: useCreatePrompterReply,
|
||||
[TaskType.rank_assistant_replies]: useRankAssistantRepliesTask,
|
||||
[TaskType.rank_initial_prompts]: useRankInitialPromptsTask,
|
||||
[TaskType.rank_prompter_replies]: useRankPrompterRepliesTask,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const getTypeSafei18nKey = (key: string) => key as unknown as TemplateStringsArray;
|
||||
+151
-124
@@ -1,14 +1,14 @@
|
||||
import type { Message } from "src/types/Conversation";
|
||||
import type { EmojiOp, Message } from "src/types/Conversation";
|
||||
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
import type { AvailableTasks } from "src/types/Task";
|
||||
import type { BackendUser, BackendUserCore } 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;
|
||||
@@ -18,110 +18,35 @@ export class OasstError {
|
||||
export class OasstApiClient {
|
||||
oasstApiUrl: string;
|
||||
oasstApiKey: string;
|
||||
userHeaders: Record<string, string> = {};
|
||||
|
||||
constructor(oasstApiUrl: string, oasstApiKey: string) {
|
||||
constructor(oasstApiUrl: string, oasstApiKey: string, user?: BackendUserCore) {
|
||||
this.oasstApiUrl = oasstApiUrl;
|
||||
this.oasstApiKey = oasstApiKey;
|
||||
if (user) {
|
||||
this.userHeaders = {
|
||||
"X-OASST-USER": `${user.auth_method}:${user.id}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async post(path: string, body: any): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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.
|
||||
async fetchTask(taskType: string, user: BackendUserCore): Promise<any> {
|
||||
async fetchTask(taskType: string, user: BackendUserCore, lang: string): Promise<any> {
|
||||
return this.post("/api/v1/tasks/", {
|
||||
type: taskType,
|
||||
user,
|
||||
lang,
|
||||
});
|
||||
}
|
||||
|
||||
async ackTask(taskId: string, messageId: string): Promise<void> {
|
||||
async ackTask(taskId: string, messageId: string): Promise<null> {
|
||||
return this.post(`/api/v1/tasks/${taskId}/ack`, {
|
||||
message_id: messageId,
|
||||
});
|
||||
}
|
||||
|
||||
async nackTask(taskId: string, reason: string): Promise<void> {
|
||||
async nackTask(taskId: string, reason: string): Promise<null> {
|
||||
return this.post(`/api/v1/tasks/${taskId}/nack`, {
|
||||
reason,
|
||||
});
|
||||
@@ -136,7 +61,8 @@ export class OasstApiClient {
|
||||
messageId: string,
|
||||
userMessageId: string,
|
||||
content: object,
|
||||
user: BackendUserCore
|
||||
user: BackendUserCore,
|
||||
lang: string
|
||||
): Promise<any> {
|
||||
return this.post("/api/v1/tasks/interaction", {
|
||||
type: updateType,
|
||||
@@ -144,6 +70,7 @@ export class OasstApiClient {
|
||||
task_id: taskId,
|
||||
message_id: messageId,
|
||||
user_message_id: userMessageId,
|
||||
lang,
|
||||
...content,
|
||||
});
|
||||
}
|
||||
@@ -151,8 +78,29 @@ export class OasstApiClient {
|
||||
/**
|
||||
* Returns the tasks availability information for given `user`.
|
||||
*/
|
||||
async fetch_tasks_availability(user: object): Promise<any> {
|
||||
return this.post("/api/v1/tasks/availability", user);
|
||||
async fetch_tasks_availability(user: object): Promise<AvailableTasks | null> {
|
||||
return this.post<AvailableTasks>("/api/v1/tasks/availability", user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `Message`s associated with `user_id` in the backend.
|
||||
*/
|
||||
async fetch_message(message_id: string, user: BackendUserCore): Promise<Message> {
|
||||
return this.get<Message>(`/api/v1/messages/${message_id}?username=${user.id}&auth_method=${user.auth_method}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a report about a message
|
||||
*/
|
||||
async send_report(message_id: string, user: BackendUserCore, text: string) {
|
||||
return this.post("/api/v1/text_labels", {
|
||||
type: "text_labels",
|
||||
message_id,
|
||||
labels: [], // Not yet implemented
|
||||
text,
|
||||
is_report: true,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,46 +120,41 @@ export class OasstApiClient {
|
||||
/**
|
||||
* Returns the `BackendUser` associated with `user_id`
|
||||
*/
|
||||
async fetch_user(user_id: string): Promise<BackendUser> {
|
||||
async fetch_user(user_id: string): Promise<BackendUser | null> {
|
||||
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<BackendUser[]>} A Promise that returns an array of `BackendUser` objects.
|
||||
*/
|
||||
async fetch_users(max_count: number, cursor: string, isForward: boolean): Promise<BackendUser[]> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("max_count", max_count.toString());
|
||||
|
||||
// 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(isForward ? "gt" : "lt", cursor);
|
||||
}
|
||||
const BASE_URL = `/api/v1/frontend_users`;
|
||||
const url = `${BASE_URL}/?${params.toString()}`;
|
||||
return this.get(url);
|
||||
async fetch_users({
|
||||
direction,
|
||||
limit,
|
||||
cursor,
|
||||
searchDisplayName,
|
||||
sortKey = "display_name",
|
||||
}: FetchUsersParams): Promise<FetchUsersResponse | null> {
|
||||
return this.get<FetchUsersResponse>(`/api/v1/users/cursor`, {
|
||||
search_text: searchDisplayName,
|
||||
sort_key: sortKey,
|
||||
max_count: limit,
|
||||
after: direction === "forward" ? cursor : undefined,
|
||||
before: direction === "back" ? cursor : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `Message`s associated with `user_id` in the backend.
|
||||
*/
|
||||
async fetch_user_messages(user_id: string): Promise<Message[]> {
|
||||
return this.get(`/api/v1/users/${user_id}/messages`);
|
||||
async fetch_user_messages(user_id: string): Promise<Message[] | null> {
|
||||
return this.get<Message[]>(`/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<void> {
|
||||
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<void> {
|
||||
await this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}¬es=${notes}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,18 +167,102 @@ export class OasstApiClient {
|
||||
/**
|
||||
* Returns the current leaderboard ranking.
|
||||
*/
|
||||
async fetch_leaderboard(time_frame: LeaderboardTimeFrame): Promise<LeaderboardReply> {
|
||||
return this.get(`/api/v1/leaderboards/${time_frame}`);
|
||||
async fetch_leaderboard(
|
||||
time_frame: LeaderboardTimeFrame,
|
||||
{ limit = 20 }: { limit?: number }
|
||||
): Promise<LeaderboardReply | null> {
|
||||
return this.get<LeaderboardReply>(`/api/v1/leaderboards/${time_frame}`, { max_count: limit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the counts of all tasks (some might be zero)
|
||||
*/
|
||||
async fetch_available_tasks(user: BackendUserCore): Promise<AvailableTasks> {
|
||||
return this.post(`/api/v1/tasks/availability`, user);
|
||||
async fetch_available_tasks(user: BackendUserCore, lang: string): Promise<AvailableTasks | null> {
|
||||
return this.post<AvailableTasks>(`/api/v1/tasks/availability?lang=${lang}`, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/remove an emoji on a message for a user
|
||||
*/
|
||||
async set_user_message_emoji(message_id: string, user: BackendUserCore, emoji: string, op: EmojiOp): Promise<void> {
|
||||
await this.post(`/api/v1/messages/${message_id}/emoji`, {
|
||||
user,
|
||||
emoji,
|
||||
op,
|
||||
});
|
||||
}
|
||||
|
||||
private async post<T>(path: string, body: unknown) {
|
||||
return this.request<T>("POST", path, {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
private async put<T>(path: string) {
|
||||
return this.request<T>("PUT", path);
|
||||
}
|
||||
|
||||
private async get<T>(path: string, query?: Record<string, string | number | boolean | undefined>) {
|
||||
if (!query) {
|
||||
return this.request<T>("GET", path);
|
||||
}
|
||||
|
||||
const filteredQuery = Object.fromEntries(
|
||||
Object.entries(query).filter(([, value]) => value !== undefined)
|
||||
) as Record<string, string>;
|
||||
|
||||
const params = new URLSearchParams(filteredQuery).toString();
|
||||
|
||||
return this.request<T>("GET", `${path}?${params}`);
|
||||
}
|
||||
|
||||
private async request<T>(method: "GET" | "POST" | "PUT", path: string, init?: RequestInit): Promise<T | null> {
|
||||
const resp = await fetch(`${this.oasstApiUrl}${path}`, {
|
||||
method,
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
...this.userHeaders,
|
||||
"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;
|
||||
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();
|
||||
}
|
||||
|
||||
fetch_my_messages(user: BackendUserCore) {
|
||||
const params = new URLSearchParams({
|
||||
username: user.id,
|
||||
auth_method: user.auth_method,
|
||||
});
|
||||
return this.get<Message[]>(`/api/v1/messages?${params}`);
|
||||
}
|
||||
|
||||
fetch_recent_messages() {
|
||||
return this.get<Message[]>(`/api/v1/messages`);
|
||||
}
|
||||
|
||||
fetch_message_children(messageId: string) {
|
||||
return this.get<Message[]>(`/api/v1/messages/${messageId}/children`);
|
||||
}
|
||||
|
||||
fetch_conversation(messageId: string) {
|
||||
return this.get(`/api/v1/messages/${messageId}/conversation`);
|
||||
}
|
||||
}
|
||||
|
||||
const oasstApiClient = new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY);
|
||||
|
||||
export { oasstApiClient };
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import { OasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { BackendUserCore } from "src/types/Users";
|
||||
|
||||
export const createApiClientFromUser = (user: BackendUserCore) =>
|
||||
new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY, user);
|
||||
|
||||
export const createApiClient = async (token: JWT) => createApiClientFromUser(await getBackendUserCore(token.sub));
|
||||
|
||||
export const userlessApiClient = new OasstApiClient(process.env.FASTAPI_URL, process.env.FASTAPI_KEY);
|
||||
@@ -1,6 +1,32 @@
|
||||
import parser from "accept-language-parser";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { i18n } from "src/../next-i18next.config";
|
||||
import prisma from "src/lib/prismadb";
|
||||
import type { BackendUserCore } from "src/types/Users";
|
||||
|
||||
const LOCALE_SET = new Set(i18n.locales);
|
||||
|
||||
/**
|
||||
* Returns the most appropriate user language using the following priority:
|
||||
*
|
||||
* 1. The `NEXT_LOCALE` cookie which is set by the client side and will be in
|
||||
* the set of supported locales.
|
||||
* 2. The `accept-language` header if it contains a supported locale as set by
|
||||
* the i18n module.
|
||||
* 3. "en" as a final fallback.
|
||||
*/
|
||||
const getUserLanguage = (req: NextApiRequest): string => {
|
||||
const cookieLanguage = req.cookies["NEXT_LOCALE"];
|
||||
if (cookieLanguage) {
|
||||
return cookieLanguage;
|
||||
}
|
||||
const headerLanguages = parser.parse(req.headers["accept-language"]);
|
||||
if (headerLanguages.length > 0 && LOCALE_SET.has(headerLanguages[0].code)) {
|
||||
return headerLanguages[0].code;
|
||||
}
|
||||
return "en";
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a `BackendUserCore` that can be used for interacting with the Backend service.
|
||||
*
|
||||
@@ -35,4 +61,4 @@ const getBackendUserCore = async (id: string) => {
|
||||
} as BackendUserCore;
|
||||
};
|
||||
|
||||
export { getBackendUserCore };
|
||||
export { getBackendUserCore, getUserLanguage };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Center, Link, Text } from "@chakra-ui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import { FiAlertTriangle } from "react-icons/fi";
|
||||
import { EmptyState } from "src/components/EmptyState";
|
||||
import { getTransparentHeaderLayout } from "src/components/Layout";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
@@ -13,12 +13,12 @@ function Error() {
|
||||
<meta name="404" content="Sorry, this page doesn't exist." />
|
||||
</Head>
|
||||
<Center flexDirection="column" gap="4" fontSize="lg" className="subpixel-antialiased">
|
||||
<EmptyState text="Sorry, the page you are looking for does not exist." icon={FiAlertTriangle} />
|
||||
<EmptyState text="Sorry, the page you are looking for does not exist." icon={AlertTriangle} />
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap="2" mt="6">
|
||||
<Text fontSize="sm">If you were trying to contribute data but ended up here, please file a bug.</Text>
|
||||
<Button
|
||||
width="fit-content"
|
||||
leftIcon={<FiAlertTriangle className="text-blue-500" aria-hidden="true" />}
|
||||
leftIcon={<AlertTriangle size={"1em"} className="text-blue-500" aria-hidden="true" />}
|
||||
variant="solid"
|
||||
size="xs"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Center, Link, Text } from "@chakra-ui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import { FiAlertTriangle } from "react-icons/fi";
|
||||
import { EmptyState } from "src/components/EmptyState";
|
||||
import { getTransparentHeaderLayout } from "src/components/Layout";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
@@ -13,15 +13,12 @@ function ServerError() {
|
||||
<meta name="404" content="Sorry, this page doesn't exist." />
|
||||
</Head>
|
||||
<Center flexDirection="column" gap="4" fontSize="lg" className="subpixel-antialiased">
|
||||
<EmptyState
|
||||
text="Sorry, we encountered a server error. We're not sure what went wrong."
|
||||
icon={FiAlertTriangle}
|
||||
/>
|
||||
<EmptyState text="Sorry, we encountered a server error. We're not sure what went wrong." icon={AlertTriangle} />
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap="2" mt="6">
|
||||
<Text fontSize="sm">If you were trying to contribute data but ended up here, please file a bug.</Text>
|
||||
<Button
|
||||
width="fit-content"
|
||||
leftIcon={<FiAlertTriangle className="text-blue-500" aria-hidden="true" />}
|
||||
leftIcon={<AlertTriangle size="1em" className="text-blue-500" aria-hidden="true" />}
|
||||
variant="solid"
|
||||
size="xs"
|
||||
>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, Divider, Flex, Grid, Icon, Text } from "@chakra-ui/react";
|
||||
import { Divider, Flex, Grid, Icon, Text } from "@chakra-ui/react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useSession } from "next-auth/react";
|
||||
import React from "react";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
import { MdOutlineEdit } from "react-icons/md";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { SurveyCard } from "src/components/Survey/SurveyCard";
|
||||
|
||||
export default function Account() {
|
||||
@@ -34,7 +34,7 @@ export default function Account() {
|
||||
<Flex gap={2}>
|
||||
{session.user.name ?? "(No username)"}
|
||||
<Link href="/account/edit">
|
||||
<Icon boxSize={5} as={MdOutlineEdit} />
|
||||
<Icon boxSize={5} as={Pencil} size="1em" />
|
||||
</Link>
|
||||
</Flex>
|
||||
<Text as="b">Email</Text>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
import { getAdminLayout } from "src/components/Layout";
|
||||
import UsersCell from "src/components/UsersCell";
|
||||
import { UserTable } from "src/components/UserTable";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,6 @@ const AdminIndex = () => {
|
||||
}
|
||||
router.push("/");
|
||||
}, [router, session, status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -38,7 +37,7 @@ const AdminIndex = () => {
|
||||
content="Conversational AI for everyone. An open source project to create a chat enabled GPT LLM run by LAION and contributors around the world."
|
||||
/>
|
||||
</Head>
|
||||
<main className="oa-basic-theme">{status === "loading" ? "loading..." : <UsersCell />}</main>
|
||||
<main>{status === "loading" ? "loading..." : <UserTable />}</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getAdminLayout } from "src/components/Layout";
|
||||
import { Role, RoleSelect } from "src/components/RoleSelect";
|
||||
import { UserMessagesCell } from "src/components/UserMessagesCell";
|
||||
import { post } from "src/lib/api";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { userlessApiClient } from "src/lib/oasst_client_factory";
|
||||
import prisma from "src/lib/prismadb";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
|
||||
@@ -113,7 +113,7 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
|
||||
* Fetch the user's data on the server side when rendering.
|
||||
*/
|
||||
export async function getServerSideProps({ query, locale }) {
|
||||
const backend_user = await oasstApiClient.fetch_user(query.id);
|
||||
const backend_user = await userlessApiClient.fetch_user(query.id);
|
||||
const local_user = await prisma.user.findUnique({
|
||||
where: { id: backend_user.id },
|
||||
select: {
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
CardBody,
|
||||
CircularProgress,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
Table,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
@@ -19,9 +19,10 @@ import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import { getAdminLayout } from "src/components/Layout";
|
||||
import { get } from "src/lib/api";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
|
||||
/**
|
||||
* Provides the admin status page that shows result of calls to several backend API endpoints,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { withRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
|
||||
/**
|
||||
* Returns tasks availability, stats, and tree manager stats.
|
||||
*/
|
||||
const handler = withRole("admin", async (req, res) => {
|
||||
// NOTE: why are we using a dummy user here?
|
||||
const dummyUser = {
|
||||
id: "__dummy_user__",
|
||||
display_name: "Dummy User",
|
||||
auth_method: "local",
|
||||
};
|
||||
const oasstApiClient = createApiClientFromUser(dummyUser);
|
||||
const [tasksAvailabilityOutcome, statsOutcome, treeManagerOutcome] = await Promise.allSettled([
|
||||
oasstApiClient.fetch_tasks_availability(dummyUser),
|
||||
oasstApiClient.fetch_stats(),
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { withRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import prisma from "src/lib/prismadb";
|
||||
|
||||
/**
|
||||
* Update's the user's data in the database. Accessible only to admins.
|
||||
*/
|
||||
const handler = withRole("admin", async (req, res) => {
|
||||
const handler = withRole("admin", async (req, res, token) => {
|
||||
const { id, auth_method, user_id, notes, role } = req.body;
|
||||
|
||||
const oasstApiClient = await createApiClient(token);
|
||||
// If the user is authorized by the web, update their role.
|
||||
if (auth_method === "local") {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
role,
|
||||
},
|
||||
where: { id },
|
||||
data: { role },
|
||||
});
|
||||
}
|
||||
// Tell the backend the user's enabled or not enabled status.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { withRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import type { Message } from "src/types/Conversation";
|
||||
|
||||
/**
|
||||
* Returns the messages recorded by the backend for a user.
|
||||
*/
|
||||
const handler = withRole("admin", async (req, res) => {
|
||||
const handler = withRole("admin", async (req, res, token) => {
|
||||
const { user } = req.query;
|
||||
const oasstApiClient = await createApiClient(token);
|
||||
const messages: Message[] = await oasstApiClient.fetch_user_messages(user as string);
|
||||
res.status(200).json(messages);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { withRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
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.
|
||||
@@ -16,11 +17,18 @@ const PAGE_SIZE = 20;
|
||||
* - `direction`: Either "forward" or "backward" representing the pagination
|
||||
* direction.
|
||||
*/
|
||||
const handler = withRole("admin", async (req, res) => {
|
||||
const { cursor, direction } = req.query;
|
||||
const handler = withRole("admin", async (req, res, token) => {
|
||||
const { cursor, direction, searchDisplayName = "", sortKey = "username" } = req.query;
|
||||
|
||||
const oasstApiClient = await createApiClient(token);
|
||||
// First, get all the users according to the backend.
|
||||
const all_users = await oasstApiClient.fetch_users(PAGE_SIZE, cursor as string, direction === "forward");
|
||||
const { items: all_users, ...rest } = await oasstApiClient.fetch_users({
|
||||
searchDisplayName: searchDisplayName as FetchUsersParams["searchDisplayName"],
|
||||
direction: direction as FetchUsersParams["direction"],
|
||||
limit: PAGE_SIZE,
|
||||
cursor: cursor as FetchUsersParams["cursor"],
|
||||
sortKey: sortKey === "username" || sortKey === "display_name" ? sortKey : undefined,
|
||||
});
|
||||
|
||||
// Next, get all the users stored in the web's auth database to fetch their role.
|
||||
const local_user_ids = all_users.map(({ id }) => id);
|
||||
@@ -51,7 +59,10 @@ const handler = withRole("admin", async (req, res) => {
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json(users);
|
||||
res.status(200).json({
|
||||
items: users,
|
||||
...rest,
|
||||
});
|
||||
});
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const availableTasks = await oasstApiClient.fetch_available_tasks(user);
|
||||
const oasstApiClient = createApiClientFromUser(user);
|
||||
const userLanguage = getUserLanguage(req);
|
||||
const availableTasks = await oasstApiClient.fetch_available_tasks(user, userLanguage);
|
||||
res.status(200).json(availableTasks);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
|
||||
/**
|
||||
* Returns the set of valid labels that can be applied to messages.
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const oasstApiClient = await createApiClient(token);
|
||||
const time_frame = (req.query.time_frame as LeaderboardTimeFrame) ?? LeaderboardTimeFrame.day;
|
||||
const info = await oasstApiClient.fetch_leaderboard(time_frame);
|
||||
const info = await oasstApiClient.fetch_leaderboard(time_frame, { limit: req.query.limit as unknown as number });
|
||||
res.status(200).json(info);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${id}/children`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const messages = await messagesRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const client = await createApiClient(token);
|
||||
const messages = await client.fetch_message_children(id as string);
|
||||
res.status(200).json(messages);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${id}/conversation`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const messages = await messagesRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const client = await createApiClient(token);
|
||||
const messages = await client.fetch_conversation(id as string);
|
||||
res.status(200).json(messages);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const { id } = req.query;
|
||||
|
||||
if (!id) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = id as string;
|
||||
|
||||
const { emoji, op } = req.body;
|
||||
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const oasstApiClient = createApiClientFromUser(user);
|
||||
try {
|
||||
await oasstApiClient.set_user_message_emoji(messageId, user, emoji, op);
|
||||
} catch (err) {
|
||||
console.error(JSON.stringify(err));
|
||||
return res.status(500).json(err);
|
||||
}
|
||||
|
||||
// Get updated emoji
|
||||
const message = await oasstApiClient.fetch_message(messageId, user);
|
||||
res.status(200).json({ emojis: message.emojis, user_emojis: message.user_emojis });
|
||||
});
|
||||
|
||||
export default handler;
|
||||
@@ -1,18 +1,12 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const messageRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const message = await messageRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const client = createApiClientFromUser(user);
|
||||
const message = await client.fetch_message(id as string, user);
|
||||
res.status(200).json(message);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClient, createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const { id } = req.query;
|
||||
|
||||
if (!id) {
|
||||
@@ -8,32 +10,16 @@ const handler = withoutRole("banned", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const message = await messageRes.json();
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const client = createApiClientFromUser(user);
|
||||
const message = await client.fetch_message(id as string, user);
|
||||
|
||||
if (!message.parent_id) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parentRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${message.parent_id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const parent = await parentRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const parent = await client.fetch_message(message.parent_id, user);
|
||||
res.status(200).json(parent);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
},
|
||||
});
|
||||
const messages = await messagesRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const client = await createApiClient(token);
|
||||
const messages = await client.fetch_recent_messages();
|
||||
res.status(200).json(messages);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
//TODO: add params if needed
|
||||
const params = new URLSearchParams({
|
||||
username: token.sub,
|
||||
});
|
||||
|
||||
const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages?${params}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
},
|
||||
});
|
||||
const messages = await messagesRes.json();
|
||||
|
||||
// Send recieved messages to the client.
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const client = createApiClientFromUser(user);
|
||||
const messages = await client.fetch_my_messages(user);
|
||||
res.status(200).json(messages);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import prisma from "src/lib/prismadb";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
|
||||
|
||||
/**
|
||||
* Returns a new task created from the Task Backend. We do a few things here:
|
||||
@@ -14,11 +14,13 @@ import { getBackendUserCore } from "src/lib/users";
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// Fetch the new task.
|
||||
const { task_type } = req.query;
|
||||
const userLanguage = getUserLanguage(req);
|
||||
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const oasstApiClient = createApiClientFromUser(user);
|
||||
let task;
|
||||
try {
|
||||
task = await oasstApiClient.fetchTask(task_type as string, user);
|
||||
task = await oasstApiClient.fetchTask(task_type as string, user, userLanguage);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json(err);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import prisma from "src/lib/prismadb";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// Parse out the local task ID and the interaction contents.
|
||||
const { id: frontendId, reason } = req.body;
|
||||
|
||||
const registeredTask = await prisma.registeredTask.findUniqueOrThrow({ where: { id: frontendId } });
|
||||
const [oasstApiClient, registeredTask] = await Promise.all([
|
||||
createApiClient(token),
|
||||
prisma.registeredTask.findUniqueOrThrow({ where: { id: frontendId } }),
|
||||
]);
|
||||
|
||||
const task = registeredTask.task as Prisma.JsonObject;
|
||||
const id = task.id as string;
|
||||
const taskId = (registeredTask.task as Prisma.JsonObject).id as string;
|
||||
|
||||
// Update the backend with the rejection
|
||||
await oasstApiClient.nackTask(id, reason);
|
||||
await oasstApiClient.nackTask(taskId, reason);
|
||||
|
||||
// Send the results to the client.
|
||||
res.status(200).json({});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
/**
|
||||
* Adds a report for a message
|
||||
*
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// Parse out the local message_id, and the interaction contents.
|
||||
const { message_id, text } = req.body;
|
||||
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const oasstApiClient = createApiClientFromUser(user);
|
||||
try {
|
||||
await oasstApiClient.send_report(message_id, user, text);
|
||||
} catch (err) {
|
||||
console.error(JSON.stringify(err));
|
||||
return res.status(500).json(err);
|
||||
}
|
||||
|
||||
res.status(200).end();
|
||||
});
|
||||
|
||||
export default handler;
|
||||
@@ -5,8 +5,10 @@ import { withoutRole } from "src/lib/auth";
|
||||
*
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// TODO: move to oasst_api_client
|
||||
// Parse out the local message_id, and the interaction contents.
|
||||
const { message_id, label_map, text } = req.body;
|
||||
const { message_id, label_map } = req.body;
|
||||
|
||||
const interactionRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/text_labels`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -17,7 +19,8 @@ const handler = withoutRole("banned", async (req, res, token) => {
|
||||
type: "text_labels",
|
||||
message_id: message_id,
|
||||
labels: label_map,
|
||||
text: text,
|
||||
text: "", // used only in reporting
|
||||
is_report: false,
|
||||
user: {
|
||||
id: token.sub,
|
||||
display_name: token.name || token.email,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import prisma from "src/lib/prismadb";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
|
||||
|
||||
/**
|
||||
* Stores the task interaction with the Task Backend and then returns the next task generated.
|
||||
@@ -18,13 +18,18 @@ const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// Parse out the local task ID and the interaction contents.
|
||||
const { id: frontendId, content, update_type } = req.body;
|
||||
|
||||
// Record that the user has done meaningful work and is no longer new.
|
||||
await prisma.user.update({ where: { id: token.sub }, data: { isNew: false } });
|
||||
// do in parallel since they are independent
|
||||
const [_, registeredTask, oasstApiClient] = await Promise.all([
|
||||
// Record that the user has done meaningful work and is no longer new.
|
||||
prisma.user.update({ where: { id: token.sub }, data: { isNew: false } }),
|
||||
// Accept the task so that we can complete it, this will probably go away soon.
|
||||
prisma.registeredTask.findUniqueOrThrow({ where: { id: frontendId } }),
|
||||
// Create client for upcoming requests
|
||||
createApiClient(token),
|
||||
]);
|
||||
|
||||
const taskId = (registeredTask.task as Prisma.JsonObject).id as string;
|
||||
|
||||
// Accept the task so that we can complete it, this will probably go away soon.
|
||||
const registeredTask = await prisma.registeredTask.findUniqueOrThrow({ where: { id: frontendId } });
|
||||
const task = registeredTask.task as Prisma.JsonObject;
|
||||
const taskId = task.id as string;
|
||||
await oasstApiClient.ackTask(taskId, registeredTask.id);
|
||||
|
||||
// Log the interaction locally to create our user_post_id needed by the Task
|
||||
@@ -41,9 +46,18 @@ const handler = withoutRole("banned", async (req, res, token) => {
|
||||
});
|
||||
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const userLanguage = getUserLanguage(req);
|
||||
let newTask;
|
||||
try {
|
||||
newTask = await oasstApiClient.interactTask(update_type, taskId, frontendId, interaction.id, content, user);
|
||||
newTask = await oasstApiClient.interactTask(
|
||||
update_type,
|
||||
taskId,
|
||||
frontendId,
|
||||
interaction.id,
|
||||
content,
|
||||
user,
|
||||
userLanguage
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(JSON.stringify(err));
|
||||
return res.status(500).json(err);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
|
||||
/**
|
||||
* Returns the set of valid labels that can be applied to messages.
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res) => {
|
||||
const valid_labels = await oasstApiClient.fetch_valid_text();
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const client = await createApiClient(token);
|
||||
const valid_labels = await client.fetch_valid_text();
|
||||
res.status(200).json(valid_labels);
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user