mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-06-27 16:10:30 +08:00
Show current user rank in leaderboard (#1263)
close #1000 maybe #1178 too * Show current user rank in the leaderboard with +-1 user (only on leaderboard * Extend auto_main script to use random user. * Support colSpan in the DataTable component (I haven't verified colSpan in header yet, leave that until we need it) * Refactor OasstError to include the path and request method.
This commit is contained in:
+22
-17
@@ -6,12 +6,10 @@ from uuid import uuid4
|
||||
|
||||
import requests
|
||||
import typer
|
||||
from faker import Faker
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
# debug constants
|
||||
USER = {"id": "1234", "display_name": "John Doe", "auth_method": "local"}
|
||||
fake = Faker()
|
||||
|
||||
|
||||
def _random_message_id():
|
||||
@@ -26,19 +24,11 @@ def _render_message(message: dict) -> str:
|
||||
|
||||
|
||||
@app.command()
|
||||
def main(backend_url: str = "http://127.0.0.1:8080", api_key: str = "1234"):
|
||||
def main(
|
||||
backend_url: str = "http://127.0.0.1:8080", api_key: str = "1234", random_users: int = 1, task_per_user: int = 10
|
||||
):
|
||||
"""automates tasks"""
|
||||
|
||||
# make sure dummy user has accepted the terms of service
|
||||
create_user_request = dict(USER)
|
||||
create_user_request["tos_acceptance"] = True
|
||||
response = requests.post(
|
||||
f"{backend_url}/api/v1/frontend_users/", json=create_user_request, headers={"X-API-Key": api_key}
|
||||
)
|
||||
response.raise_for_status()
|
||||
user = response.json()
|
||||
typer.echo(f"user: {user}")
|
||||
|
||||
def _post(path: str, json: dict) -> dict:
|
||||
response = requests.post(f"{backend_url}{path}", json=json, headers={"X-API-Key": api_key})
|
||||
response.raise_for_status()
|
||||
@@ -60,8 +50,23 @@ def main(backend_url: str = "http://127.0.0.1:8080", api_key: str = "1234"):
|
||||
print(shuffled)
|
||||
return ranks
|
||||
|
||||
tasks = [_post("/api/v1/tasks/", {"type": "random", "user": USER})]
|
||||
for i in range(int(random_users)):
|
||||
name = fake.name()
|
||||
USER = {"id": name, "display_name": name, "auth_method": "local"}
|
||||
|
||||
create_user_request = dict(USER)
|
||||
# make sure dummy user has accepted the terms of service
|
||||
create_user_request["tos_acceptance"] = True
|
||||
response = requests.post(
|
||||
f"{backend_url}/api/v1/frontend_users/", json=create_user_request, headers={"X-API-Key": api_key}
|
||||
)
|
||||
response.raise_for_status()
|
||||
user = response.json()
|
||||
typer.echo(f"user: {user}")
|
||||
q = 0
|
||||
|
||||
tasks = [_post("/api/v1/tasks/", {"type": "random", "user": USER})]
|
||||
|
||||
while tasks:
|
||||
task = tasks.pop(0)
|
||||
print(task)
|
||||
@@ -250,7 +255,7 @@ def main(backend_url: str = "http://127.0.0.1:8080", api_key: str = "1234"):
|
||||
# rerun with new task selected from above cases
|
||||
# add a new task
|
||||
q += 1
|
||||
if q == 10:
|
||||
if q == task_per_user:
|
||||
typer.echo("Task done!")
|
||||
break
|
||||
tasks = [_post("/api/v1/tasks/", {"type": "random", "user": USER})]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
faker==16.6.1
|
||||
requests==2.28.1
|
||||
typer==0.7.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, CardBody, Link, Text } from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import NextLink from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { LeaderboardTable } from "src/components/LeaderboardTable";
|
||||
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
|
||||
@@ -19,7 +19,7 @@ export function LeaderboardWidget() {
|
||||
</div>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<LeaderboardTable timeFrame={LeaderboardTimeFrame.day} limit={5} rowPerPage={5} />
|
||||
<LeaderboardTable timeFrame={LeaderboardTimeFrame.day} limit={5} rowPerPage={5} hideCurrentUserRanking />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
Tr,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table";
|
||||
import { Cell, ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table";
|
||||
import { Filter } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { ChangeEvent, ReactNode } from "react";
|
||||
@@ -31,6 +31,7 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export type DataTableColumnDef<T> = ColumnDef<T> & {
|
||||
filterable?: boolean;
|
||||
span?: number | ((cell: Cell<T, unknown>) => number | undefined);
|
||||
};
|
||||
|
||||
// TODO: stricter type
|
||||
@@ -126,9 +127,7 @@ export const DataTable = <T,>({
|
||||
const props = typeof rowProps === "function" ? rowProps(row) : rowProps;
|
||||
return (
|
||||
<Tr key={row.id} {...props}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Td>
|
||||
))}
|
||||
<DataTableRow row={row}></DataTableRow>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
@@ -139,6 +138,36 @@ export const DataTable = <T,>({
|
||||
);
|
||||
};
|
||||
|
||||
type WithSpanCell<T> = Cell<T, unknown> & { span?: number };
|
||||
|
||||
const DataTableRow = <T,>({ row }: { row: Row<T> }) => {
|
||||
const cells: WithSpanCell<T>[] = row.getVisibleCells();
|
||||
const renderCells: WithSpanCell<T>[] = [];
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
const span = (cell.column.columnDef as DataTableColumnDef<T>).span;
|
||||
const spanValue = typeof span === "function" ? span(cell) : span;
|
||||
if (spanValue && spanValue > 1) {
|
||||
i += spanValue - 1; // skip next `spanValue - 1` cell
|
||||
}
|
||||
cell.span = spanValue;
|
||||
renderCells.push(cell);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderCells.map((cell) => {
|
||||
return (
|
||||
<Td key={cell.id} colSpan={cell.span}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterModal = ({
|
||||
label,
|
||||
onChange,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CircularProgress, useColorModeValue, useToken } from "@chakra-ui/react";
|
||||
import { Box, CircularProgress, Flex, useColorModeValue, useToken } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { get } from "src/lib/api";
|
||||
@@ -7,9 +8,11 @@ import { colors } from "src/styles/Theme/colors";
|
||||
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import { DataTable, DataTableRowPropsCallback } from "../DataTable";
|
||||
import { DataTable, DataTableColumnDef, DataTableRowPropsCallback } from "../DataTable";
|
||||
|
||||
const columnHelper = createColumnHelper<LeaderboardEntity>();
|
||||
type WindowLeaderboardEntity = LeaderboardEntity & { isSpaceRow?: boolean };
|
||||
|
||||
const columnHelper = createColumnHelper<WindowLeaderboardEntity>();
|
||||
|
||||
/**
|
||||
* Presents a grid of leaderboard entries with more detailed information.
|
||||
@@ -18,10 +21,12 @@ export const LeaderboardTable = ({
|
||||
timeFrame,
|
||||
limit: limit,
|
||||
rowPerPage,
|
||||
hideCurrentUserRanking,
|
||||
}: {
|
||||
timeFrame: LeaderboardTimeFrame;
|
||||
limit: number;
|
||||
rowPerPage: number;
|
||||
hideCurrentUserRanking?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation("leaderboard");
|
||||
|
||||
@@ -29,15 +34,19 @@ export const LeaderboardTable = ({
|
||||
data: reply,
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWRImmutable<LeaderboardReply>(`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}`, get, {
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const columns = useMemo(
|
||||
} = useSWRImmutable<LeaderboardReply & { user_stats_window: LeaderboardReply["leaderboard"] }>(
|
||||
`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}`,
|
||||
get
|
||||
);
|
||||
const columns: DataTableColumnDef<WindowLeaderboardEntity>[] = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("rank", {
|
||||
{
|
||||
...columnHelper.accessor("rank", {
|
||||
header: t("rank"),
|
||||
cell: ({ row, getValue }) => (row.original.isSpaceRow ? <SpaceRow></SpaceRow> : getValue()),
|
||||
}),
|
||||
span: (cell) => (cell.row.original.isSpaceRow ? 6 : undefined),
|
||||
},
|
||||
columnHelper.accessor("display_name", {
|
||||
header: t("user"),
|
||||
}),
|
||||
@@ -63,15 +72,72 @@ export const LeaderboardTable = ({
|
||||
}, [t, reply?.last_updated]);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const data = useMemo(() => {
|
||||
const data: WindowLeaderboardEntity[] = useMemo(() => {
|
||||
if (!reply) {
|
||||
return [];
|
||||
}
|
||||
const start = (page - 1) * rowPerPage;
|
||||
return reply?.leaderboard.slice(start, start + rowPerPage) || [];
|
||||
}, [rowPerPage, page, reply?.leaderboard]);
|
||||
const end = start + rowPerPage;
|
||||
const leaderBoardEntities = reply.leaderboard.slice(start, end);
|
||||
if (hideCurrentUserRanking) {
|
||||
return leaderBoardEntities;
|
||||
}
|
||||
const userStatsWindow: WindowLeaderboardEntity[] = reply.user_stats_window;
|
||||
const userStats = userStatsWindow.find((stats) => stats.highlighted);
|
||||
if (userStats.rank > end) {
|
||||
leaderBoardEntities.push(
|
||||
{ isSpaceRow: true } as WindowLeaderboardEntity,
|
||||
...reply.user_stats_window.filter(
|
||||
(stats) =>
|
||||
leaderBoardEntities.findIndex((leaderBoardEntity) => leaderBoardEntity.user_id === stats.user_id) === -1
|
||||
) // filter to avoid duplicated row
|
||||
);
|
||||
}
|
||||
return leaderBoardEntities;
|
||||
}, [page, rowPerPage, reply, hideCurrentUserRanking]);
|
||||
|
||||
const rowProps = useLeaderboardRowProps();
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress isIndeterminate></CircularProgress>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <span>Unable to load leaderboard</span>;
|
||||
}
|
||||
|
||||
const maxPage = Math.ceil(reply.leaderboard.length / rowPerPage);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
caption={lastUpdated}
|
||||
disablePagination={limit <= rowPerPage}
|
||||
disableNext={page >= maxPage}
|
||||
disablePrevious={page === 1}
|
||||
onNextClick={() => setPage((p) => p + 1)}
|
||||
onPreviousClick={() => setPage((p) => p - 1)}
|
||||
rowProps={rowProps}
|
||||
></DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
const SpaceRow = () => {
|
||||
const color = useColorModeValue("gray.600", "gray.400");
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<Box as={MoreHorizontal} color={color}></Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const useLeaderboardRowProps = () => {
|
||||
const borderColor = useToken("colors", useColorModeValue(colors.light.active, colors.dark.active));
|
||||
const rowProps = useCallback<DataTableRowPropsCallback<LeaderboardEntity>>(
|
||||
return useCallback<DataTableRowPropsCallback<WindowLeaderboardEntity>>(
|
||||
(row) => {
|
||||
return row.original.highlighted
|
||||
const rowData = row.original;
|
||||
return rowData.highlighted
|
||||
? {
|
||||
sx: {
|
||||
// https://stackoverflow.com/questions/37963524/how-to-apply-border-radius-to-tr-in-bootstrap
|
||||
@@ -93,28 +159,4 @@ export const LeaderboardTable = ({
|
||||
},
|
||||
[borderColor]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress isIndeterminate></CircularProgress>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <span>Unable to load leaderboard</span>;
|
||||
}
|
||||
|
||||
const maxPage = Math.ceil(reply.leaderboard.length / rowPerPage);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
caption={lastUpdated}
|
||||
disablePagination={limit <= rowPerPage}
|
||||
disableNext={page === maxPage}
|
||||
disablePrevious={page === 1}
|
||||
onNextClick={() => setPage((p) => p + 1)}
|
||||
onPreviousClick={() => setPage((p) => p - 1)}
|
||||
rowProps={rowProps}
|
||||
></DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,13 @@ api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const err = error?.response?.data;
|
||||
throw new OasstError(err?.message ?? error, err?.errorCode, error?.response?.httpStatusCode || -1);
|
||||
throw new OasstError({
|
||||
message: err?.message ?? error,
|
||||
errorCode: err?.errorCode,
|
||||
httpStatusCode: error?.response?.httpStatusCode || -1,
|
||||
method: err?.config?.method,
|
||||
path: err?.config?.url,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -7,11 +7,27 @@ export class OasstError {
|
||||
message: string;
|
||||
errorCode: number;
|
||||
httpStatusCode: number;
|
||||
path: string;
|
||||
method: string;
|
||||
|
||||
constructor(message: string, errorCode: number, httpStatusCode: number) {
|
||||
constructor({
|
||||
errorCode,
|
||||
httpStatusCode,
|
||||
message,
|
||||
path,
|
||||
method,
|
||||
}: {
|
||||
message: string;
|
||||
errorCode: number;
|
||||
httpStatusCode: number;
|
||||
path: string;
|
||||
method: string;
|
||||
}) {
|
||||
this.message = message;
|
||||
this.errorCode = errorCode;
|
||||
this.httpStatusCode = httpStatusCode;
|
||||
this.path = path;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -60,9 +76,21 @@ export class OasstApiClient {
|
||||
try {
|
||||
error = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
throw new OasstError(errorText, 0, resp.status);
|
||||
throw new OasstError({
|
||||
message: errorText,
|
||||
errorCode: 0,
|
||||
httpStatusCode: resp.status,
|
||||
path,
|
||||
method,
|
||||
});
|
||||
}
|
||||
throw new OasstError(error.message ?? error, error.error_code, resp.status);
|
||||
throw new OasstError({
|
||||
message: error.message ?? error,
|
||||
errorCode: error.error_code,
|
||||
httpStatusCode: resp.status,
|
||||
path,
|
||||
method,
|
||||
});
|
||||
}
|
||||
|
||||
return resp.json();
|
||||
@@ -297,9 +325,9 @@ export class OasstApiClient {
|
||||
return this.get(`/api/v1/messages/${messageId}/conversation`);
|
||||
}
|
||||
|
||||
async fetch_tos_acceptance(user: BackendUserCore): Promise<BackendUser["tos_acceptance_date"]> {
|
||||
const backendUser = await this.get<BackendUser>(`/api/v1/frontend_users/${user.auth_method}/${user.id}`);
|
||||
return backendUser.tos_acceptance_date;
|
||||
async fetch_tos_acceptance(backendUserCore: BackendUserCore): Promise<BackendUser["tos_acceptance_date"]> {
|
||||
const user = await this.fetch_frontend_user(backendUserCore);
|
||||
return user.tos_acceptance_date;
|
||||
}
|
||||
|
||||
async set_tos_acceptance(user: BackendUserCore) {
|
||||
@@ -312,4 +340,14 @@ export class OasstApiClient {
|
||||
const backendUser = await this.get<BackendUser>(`/api/v1/frontend_users/${user.auth_method}/${user.id}`);
|
||||
return this.get(`/api/v1/users/${backendUser.user_id}/stats`);
|
||||
}
|
||||
|
||||
fetch_user_stats_window(user_id: string, time_frame: LeaderboardTimeFrame, window_size?: number) {
|
||||
return this.get<LeaderboardReply>(`/api/v1/users/${user_id}/stats/${time_frame}/window`, {
|
||||
window_size,
|
||||
});
|
||||
}
|
||||
|
||||
fetch_frontend_user(user: BackendUserCore) {
|
||||
return this.get<BackendUser>(`/api/v1/frontend_users/${user.auth_method}/${user.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
|
||||
/**
|
||||
@@ -7,9 +8,29 @@ import { LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
const oasstApiClient = await createApiClient(token);
|
||||
const backendUser = await getBackendUserCore(token.sub);
|
||||
const time_frame = (req.query.time_frame as LeaderboardTimeFrame) ?? LeaderboardTimeFrame.day;
|
||||
const info = await oasstApiClient.fetch_leaderboard(time_frame, { limit: req.query.limit as unknown as number });
|
||||
res.status(200).json(info);
|
||||
const includeUserStats = req.query.includeUserStats;
|
||||
|
||||
if (includeUserStats !== "true") {
|
||||
const leaderboard = await oasstApiClient.fetch_leaderboard(time_frame, {
|
||||
limit: req.query.limit as unknown as number,
|
||||
});
|
||||
return res.status(200).json(leaderboard);
|
||||
}
|
||||
const user = await oasstApiClient.fetch_frontend_user(backendUser);
|
||||
|
||||
const [leaderboard, user_stats] = await Promise.all([
|
||||
oasstApiClient.fetch_leaderboard(time_frame, {
|
||||
limit: req.query.limit as unknown as number,
|
||||
}),
|
||||
oasstApiClient.fetch_user_stats_window(user.user_id, time_frame, 3),
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
...leaderboard,
|
||||
user_stats_window: user_stats.leaderboard.map((stats) => ({ ...stats, is_window: true })),
|
||||
});
|
||||
});
|
||||
|
||||
export default handler;
|
||||
|
||||
Reference in New Issue
Block a user