mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-06-27 16:10:30 +08:00
Merge branch 'main' into lucide
This commit is contained in:
@@ -207,6 +207,7 @@ class UserRepository:
|
||||
limit: Optional[int] = 100,
|
||||
desc: bool = False,
|
||||
) -> list[User]:
|
||||
|
||||
if not self.api_client.trusted:
|
||||
if not api_client_id:
|
||||
# Let unprivileged api clients query their own users without api_client_id being set
|
||||
|
||||
Generated
+45
@@ -21,6 +21,7 @@
|
||||
"@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",
|
||||
@@ -12300,6 +12301,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",
|
||||
@@ -46715,6 +46747,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",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -1,51 +1,75 @@
|
||||
import { Box, Flex, GridItem, Heading, SimpleGrid, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
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 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>
|
||||
<Heading size="lg" className="pb-4">
|
||||
{TaskCategoryLabels[category]}
|
||||
</Heading>
|
||||
<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.label} href={item.pathname}>
|
||||
<GridItem
|
||||
bg={backgroundColor}
|
||||
borderRadius="xl"
|
||||
boxShadow="base"
|
||||
className="flex flex-col justify-between h-full"
|
||||
>
|
||||
<Text fontWeight="bold" color="white">
|
||||
<Flex className="p-6 pb-10" flexDir="column" gap="3">
|
||||
<Heading size="md">{item.label}</Heading>
|
||||
<Text size="sm">{item.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"
|
||||
>
|
||||
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],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
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 { ChangeEvent, ReactNode } from "react";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
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;
|
||||
};
|
||||
|
||||
export const DataTable = <T,>({
|
||||
data,
|
||||
columns,
|
||||
caption,
|
||||
filterValues = [],
|
||||
onNextClick,
|
||||
onPreviousClick,
|
||||
onFilterChange,
|
||||
disableNext,
|
||||
disablePrevious,
|
||||
}: 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 (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<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>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
<FaFilter></FaFilter>
|
||||
</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,6 +1,6 @@
|
||||
import { Box, Link, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { Box, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { AlertTriangle, LucideIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import NextLink from "next/link";
|
||||
|
||||
type EmptyStateProps = {
|
||||
text: string;
|
||||
@@ -9,16 +9,15 @@ type EmptyStateProps = {
|
||||
|
||||
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 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>
|
||||
<NextLink href="/dashboard">
|
||||
<Text color="blue.500">Go back to the dashboard</Text>
|
||||
</NextLink>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ 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";
|
||||
@@ -29,7 +29,7 @@ export const Task = ({ frontendId, task, trigger, mutate }) => {
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -21,16 +21,16 @@ export interface TaskInfo {
|
||||
}
|
||||
|
||||
export const TaskCategoryLabels: { [key in TaskCategory]: string } = {
|
||||
[TaskCategory.Random]: "I'm feeling lucky",
|
||||
[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",
|
||||
label: "I'm feeling lucky",
|
||||
desc: "Help us improve Open Assistant by starting a random task.",
|
||||
category: TaskCategory.Random,
|
||||
pathname: "/tasks/random",
|
||||
@@ -104,7 +104,7 @@ export const TaskTypes: TaskInfo[] = [
|
||||
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.",
|
||||
overview: "Given the following initial 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",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import Link from "next/link";
|
||||
import { memo, useState } from "react";
|
||||
import { FaPen } from "react-icons/fa";
|
||||
import { get } from "src/lib/api";
|
||||
import { FetchUsersResponse } from "src/lib/oasst_api_client";
|
||||
import type { 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={<FaPen></FaPen>}
|
||||
></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 (
|
||||
<>
|
||||
<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."}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Message } from "src/types/Conversation";
|
||||
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||
import type { AvailableTasks } from "src/types/Task";
|
||||
import type { BackendUser, BackendUserCore } from "src/types/Users";
|
||||
import type { BackendUser, BackendUserCore, User } from "src/types/Users";
|
||||
|
||||
export class OasstError {
|
||||
message: string;
|
||||
@@ -15,6 +15,22 @@ export class OasstError {
|
||||
}
|
||||
}
|
||||
|
||||
export type FetchUsersParams = {
|
||||
limit: number;
|
||||
cursor?: string;
|
||||
direction: "forward" | "back";
|
||||
searchDisplayName?: string;
|
||||
sortKey?: "username" | "display_name";
|
||||
};
|
||||
|
||||
export type FetchUsersResponse<T extends User | BackendUser = BackendUser> = {
|
||||
items: T[];
|
||||
next?: string;
|
||||
prev?: string;
|
||||
sort_key: "username" | "display_name";
|
||||
order: "asc" | "desc";
|
||||
};
|
||||
|
||||
export class OasstApiClient {
|
||||
oasstApiUrl: string;
|
||||
oasstApiKey: string;
|
||||
@@ -188,21 +204,40 @@ export class OasstApiClient {
|
||||
* 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());
|
||||
async fetch_users({
|
||||
direction,
|
||||
limit,
|
||||
cursor,
|
||||
searchDisplayName,
|
||||
sortKey = "display_name",
|
||||
}: FetchUsersParams): Promise<FetchUsersResponse> {
|
||||
const params = new URLSearchParams({
|
||||
search_text: searchDisplayName,
|
||||
sort_key: sortKey,
|
||||
max_count: limit.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);
|
||||
params.append(direction === "forward" ? "gt" : "lt", cursor);
|
||||
}
|
||||
const BASE_URL = `/api/v1/frontend_users`;
|
||||
const BASE_URL = `/api/v1/users/cursor`;
|
||||
const url = `${BASE_URL}/?${params.toString()}`;
|
||||
return this.get(url);
|
||||
}
|
||||
|
||||
// async fetch_user_by_display_name(name: string): Promise<BackendUser[]> {
|
||||
// const params = new URLSearchParams({
|
||||
// search_text: name,
|
||||
// });
|
||||
|
||||
// const endpoint = `/api/v1/frontend_users/by_display_name`;
|
||||
|
||||
// return this.get(`${endpoint}?${params.toString()}`);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Returns the `Message`s associated with `user_id` in the backend.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import { FetchUsersParams, oasstApiClient } from "src/lib/oasst_api_client";
|
||||
import prisma from "src/lib/prismadb";
|
||||
|
||||
/**
|
||||
@@ -17,10 +17,16 @@ const PAGE_SIZE = 20;
|
||||
* direction.
|
||||
*/
|
||||
const handler = withRole("admin", async (req, res) => {
|
||||
const { cursor, direction } = req.query;
|
||||
const { cursor, direction, searchDisplayName = "", sortKey = "username" } = req.query;
|
||||
|
||||
// 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 +57,10 @@ const handler = withRole("admin", async (req, res) => {
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json(users);
|
||||
res.status(200).json({
|
||||
items: users,
|
||||
...rest,
|
||||
});
|
||||
});
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { getBackendUserCore } from "src/lib/users";
|
||||
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
//TODO: add params if needed
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const params = new URLSearchParams({
|
||||
username: token.sub,
|
||||
username: user.id,
|
||||
auth_method: user.auth_method,
|
||||
});
|
||||
|
||||
const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages?${params}`, {
|
||||
|
||||
@@ -5,15 +5,17 @@ import { LeaderboardTable, TaskOption, WelcomeCard } from "src/components/Dashbo
|
||||
import { getDashboardLayout } from "src/components/Layout";
|
||||
import { TaskCategory } from "src/components/Tasks/TaskTypes";
|
||||
import { get } from "src/lib/api";
|
||||
import type { AvailableTasks, TaskType } from "src/types/Task";
|
||||
import { AvailableTasks, TaskType } from "src/types/Task";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { data } = useSWRImmutable<AvailableTasks>("/api/available_tasks", get);
|
||||
|
||||
// TODO: show only these tasks:
|
||||
const availableTasks = useMemo(() => filterAvailableTasks(data ?? {}), [data]);
|
||||
const availableTaskTypes = useMemo(() => {
|
||||
const taskTypes = filterAvailableTasks(data ?? {});
|
||||
return { [TaskCategory.Random]: taskTypes };
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,7 +25,7 @@ const Dashboard = () => {
|
||||
</Head>
|
||||
<Flex direction="column" gap="10">
|
||||
<WelcomeCard />
|
||||
<TaskOption displayTaskCategories={[TaskCategory.Random]} />
|
||||
<TaskOption content={availableTaskTypes} />
|
||||
<LeaderboardTable />
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Head from "next/head";
|
||||
import { TaskOption } from "src/components/Dashboard";
|
||||
import { allTaskOptions } from "src/components/Dashboard/TaskOption";
|
||||
import { getDashboardLayout } from "src/components/Layout";
|
||||
import { TaskCategory } from "src/components/Tasks/TaskTypes";
|
||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||
|
||||
const AllTasks = () => {
|
||||
@@ -11,7 +11,7 @@ const AllTasks = () => {
|
||||
<title>All Tasks - Open Assistant</title>
|
||||
<meta name="description" content="All tasks for Open Assistant." />
|
||||
</Head>
|
||||
<TaskOption displayTaskCategories={[TaskCategory.Create, TaskCategory.Evaluate, TaskCategory.Label]} />
|
||||
<TaskOption content={allTaskOptions} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user