mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-06-27 16:10:30 +08:00
implement admin message detail page (#1453)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarProps,
|
||||
Box,
|
||||
HStack,
|
||||
Menu,
|
||||
@@ -15,7 +16,18 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { boolean } from "boolean";
|
||||
import { ClipboardList, Copy, Flag, Link, MessageSquare, MoreHorizontal, Slash, Trash, User } from "lucide-react";
|
||||
import {
|
||||
ClipboardList,
|
||||
Copy,
|
||||
Flag,
|
||||
Link,
|
||||
MessageSquare,
|
||||
MoreHorizontal,
|
||||
Shield,
|
||||
Slash,
|
||||
Trash,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -24,6 +36,7 @@ import { MessageEmojiButton } from "src/components/Messages/MessageEmojiButton";
|
||||
import { ReportPopup } from "src/components/Messages/ReportPopup";
|
||||
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
|
||||
import { del, post, put } from "src/lib/api";
|
||||
import { ROUTES } from "src/lib/routes";
|
||||
import { colors } from "src/styles/Theme/colors";
|
||||
import { Message, MessageEmojis } from "src/types/Conversation";
|
||||
import { emojiIcons, isKnownEmoji } from "src/types/Emoji";
|
||||
@@ -34,9 +47,17 @@ interface MessageTableEntryProps {
|
||||
message: Message;
|
||||
enabled?: boolean;
|
||||
highlight?: boolean;
|
||||
avartarPosition?: "middle" | "top";
|
||||
avartarProps?: AvatarProps;
|
||||
}
|
||||
|
||||
export function MessageTableEntry({ message, enabled, highlight }: MessageTableEntryProps) {
|
||||
export function MessageTableEntry({
|
||||
message,
|
||||
enabled,
|
||||
highlight,
|
||||
avartarPosition = "middle",
|
||||
avartarProps,
|
||||
}: MessageTableEntryProps) {
|
||||
const router = useRouter();
|
||||
const [emojiState, setEmojis] = useState<MessageEmojis>({ emojis: {}, user_emojis: [] });
|
||||
useEffect(() => {
|
||||
@@ -68,9 +89,10 @@ export function MessageTableEntry({ message, enabled, highlight }: MessageTableE
|
||||
mr={inlineAvatar ? 2 : 0}
|
||||
name={`${boolean(message.is_assistant) ? "Assistant" : "User"}`}
|
||||
src={`${boolean(message.is_assistant) ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`}
|
||||
{...avartarProps}
|
||||
/>
|
||||
),
|
||||
[borderColor, inlineAvatar, message.is_assistant]
|
||||
[avartarProps, borderColor, inlineAvatar, message.is_assistant]
|
||||
);
|
||||
const highlightColor = useColorModeValue(colors.light.active, colors.dark.active);
|
||||
|
||||
@@ -86,13 +108,17 @@ export function MessageTableEntry({ message, enabled, highlight }: MessageTableE
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack w={["full", "full", "full", "fit-content"]} gap={2}>
|
||||
<HStack
|
||||
w={["full", "full", "full", "fit-content"]}
|
||||
gap={0.5}
|
||||
alignItems={avartarPosition === "top" ? "start" : "center"}
|
||||
>
|
||||
{!inlineAvatar && avatar}
|
||||
<Box
|
||||
width={["full", "full", "full", "fit-content"]}
|
||||
maxWidth={["full", "full", "full", "2xl"]}
|
||||
p="4"
|
||||
borderRadius="md"
|
||||
borderRadius="18px"
|
||||
bg={message.is_assistant ? backgroundColor : backgroundColor2}
|
||||
outline={highlight && "2px solid black"}
|
||||
outlineColor={highlightColor}
|
||||
@@ -249,6 +275,9 @@ const MessageActions = ({
|
||||
<MenuItem onClick={() => handleCopy(id)} icon={<Copy />}>
|
||||
{t("copy_message_id")}
|
||||
</MenuItem>
|
||||
<MenuItem as="a" href={ROUTES.ADMIN_MESSAGE_DETAIL(message.id)} target="_blank" icon={<Shield />}>
|
||||
View in admin area
|
||||
</MenuItem>
|
||||
<MenuItem as="a" href={`/admin/manage_user/${message.user_id}`} target="_blank" icon={<User />}>
|
||||
{t("view_user")}
|
||||
</MenuItem>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { Fragment } from "react";
|
||||
import { MessageWithChildren } from "src/types/Conversation";
|
||||
|
||||
import { MessageTableEntry } from "./MessageTableEntry";
|
||||
|
||||
const connectionColor = "gray.300";
|
||||
const messagePaddingTop = 16;
|
||||
const avatarSize = 32;
|
||||
const avartarMarginTop = 6;
|
||||
const maxDepth = 100; // this only used for debug UI in mobile
|
||||
const left = avatarSize / 2 - 1;
|
||||
|
||||
export const MessageTree = ({ tree, messageId }: { tree: MessageWithChildren; messageId?: string }) => {
|
||||
const renderChildren = (children: MessageWithChildren[], depth = 1) => {
|
||||
const hasSibling = children.length > 1;
|
||||
return children.map((child, idx) => {
|
||||
const hasChildren = child.children.length > 0;
|
||||
const isLastChild = idx === children.length - 1;
|
||||
return (
|
||||
<Fragment key={child.id}>
|
||||
<Box position="relative" className="box2">
|
||||
<ConnectionCurve></ConnectionCurve>
|
||||
<Box paddingLeft={`32px`} position="relative" className="box3">
|
||||
{hasSibling && !isLastChild && (
|
||||
<Box
|
||||
height={`calc(100% - 26px)`}
|
||||
position="absolute"
|
||||
width="2px"
|
||||
bg="gray.300"
|
||||
left={`${left}px`}
|
||||
top="26px"
|
||||
></Box>
|
||||
)}
|
||||
<Box pt={`${messagePaddingTop}px`} position="relative" className="box4">
|
||||
{hasChildren && depth < maxDepth && <Connection className="connection1"></Connection>}
|
||||
<MessageTableEntry
|
||||
avartarProps={{
|
||||
mt: `${avartarMarginTop}px`,
|
||||
}}
|
||||
avartarPosition="top"
|
||||
highlight={child.id === messageId}
|
||||
message={child}
|
||||
></MessageTableEntry>
|
||||
</Box>
|
||||
{depth < maxDepth && renderChildren(child.children, depth + 1)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative">
|
||||
<Box height="full" position="absolute" width="2px" bg={connectionColor} left={`${left}px`}></Box>
|
||||
<MessageTableEntry
|
||||
message={tree}
|
||||
avartarPosition="top"
|
||||
highlight={tree.id === messageId}
|
||||
avartarProps={{
|
||||
size: "sm",
|
||||
}}
|
||||
></MessageTableEntry>
|
||||
</Box>
|
||||
{renderChildren(tree.children)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Connection = ({ className, isSibling = false }: { isSibling?: boolean; className?: string }) => {
|
||||
const top = isSibling ? `26px` : `32px`;
|
||||
return (
|
||||
<Box
|
||||
height={`calc(100% - ${top})`}
|
||||
position="absolute"
|
||||
width="2px"
|
||||
bg="gray.300"
|
||||
left={`${left}px`}
|
||||
top={top}
|
||||
className={className}
|
||||
></Box>
|
||||
);
|
||||
};
|
||||
|
||||
const height = avatarSize / 2 + avartarMarginTop + messagePaddingTop;
|
||||
const width = avatarSize / 2 + 10;
|
||||
const ConnectionCurve = () => {
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
height={`${height}px`}
|
||||
width={`${width}px`}
|
||||
left={`${left}px `}
|
||||
borderBottomWidth="2px"
|
||||
borderBottomLeftRadius="10px"
|
||||
borderLeftStyle="solid"
|
||||
borderLeftWidth="2px"
|
||||
borderColor={connectionColor}
|
||||
className="curve"
|
||||
></Box>
|
||||
);
|
||||
};
|
||||
@@ -189,6 +189,13 @@ export class OasstApiClient {
|
||||
return this.get<Message>(`/api/v1/messages/${message_id}?username=${user.id}&auth_method=${user.auth_method}`);
|
||||
}
|
||||
|
||||
async fetch_message_tree(message_id: string) {
|
||||
return this.get<{
|
||||
id: string;
|
||||
messages: Message[];
|
||||
}>(`/api/v1/messages/${message_id}/tree`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message by its id
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Card, CardBody, CardHeader, CircularProgress, Grid } from "@chakra-ui/react";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { AdminArea } from "src/components/AdminArea";
|
||||
import { JsonCard } from "src/components/JsonCard";
|
||||
import { getAdminLayout } from "src/components/Layout";
|
||||
import { MessageTree } from "src/components/Messages/MessageTree";
|
||||
import { get } from "src/lib/api";
|
||||
import { Message, MessageWithChildren } from "src/types/Conversation";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
const MessageDetail = () => {
|
||||
const router = useRouter();
|
||||
const messageId = router.query.id;
|
||||
const { data, isLoading, error } = useSWRImmutable<{
|
||||
tree: MessageWithChildren | null;
|
||||
message?: Message;
|
||||
}>(`/api/admin/messages/${messageId}/tree`, get);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Open Assistant</title>
|
||||
</Head>
|
||||
<AdminArea>
|
||||
{isLoading && <CircularProgress isIndeterminate></CircularProgress>}
|
||||
{error && "Unable to load message tree"}
|
||||
{data &&
|
||||
(data.tree === null ? (
|
||||
"Unable to build tree"
|
||||
) : (
|
||||
<Grid gap="6">
|
||||
<Card>
|
||||
<CardHeader fontWeight="bold" fontSize="xl" pb="0">
|
||||
Message Detail
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<JsonCard>{data.message}</JsonCard>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader fontWeight="bold" fontSize="xl" pb="0">
|
||||
Tree {data.tree.id}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<MessageTree tree={data.tree} messageId={data.message?.id}></MessageTree>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</AdminArea>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MessageDetail.getLayout = getAdminLayout;
|
||||
|
||||
export default MessageDetail;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale = "en" }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ["common", "labelling", "message"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { withAnyRole } from "src/lib/auth";
|
||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||
import { Message, MessageWithChildren } from "src/types/Conversation";
|
||||
|
||||
export default withAnyRole(["admin", "moderator"], async (req, res, token) => {
|
||||
const client = await createApiClient(token);
|
||||
const messageId = req.query.id as string;
|
||||
const response = await client.fetch_message_tree(messageId);
|
||||
|
||||
if (!response) {
|
||||
return res.json({ tree: null });
|
||||
}
|
||||
|
||||
const tree = buildTree(response.messages);
|
||||
|
||||
return res.json({ tree, message: response.messages.find((m) => m.id === messageId) });
|
||||
});
|
||||
|
||||
// https://medium.com/@lizhuohang.selina/building-a-hierarchical-tree-from-a-flat-list-an-easy-to-understand-solution-visualisation-19cb24bdfa33
|
||||
const buildTree = (messages: Message[]): MessageWithChildren | null => {
|
||||
const map: Record<string, MessageWithChildren> = {};
|
||||
const tree = [];
|
||||
|
||||
// Build a hash table and map items to objects
|
||||
messages.forEach(function (item) {
|
||||
const id = item.id;
|
||||
if (!map[id]) {
|
||||
map[id] = { ...item, children: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Loop over hash table
|
||||
let mappedElem: MessageWithChildren;
|
||||
for (const id in map) {
|
||||
if (map[id]) {
|
||||
mappedElem = map[id];
|
||||
|
||||
// If the element is not at the root level, add it to its parent array of children. Note this will continue till we have only root level elements left
|
||||
if (mappedElem.parent_id) {
|
||||
const parentId = mappedElem.parent_id;
|
||||
map[parentId].children.push(mappedElem);
|
||||
}
|
||||
|
||||
// If the element is at the root level, directly push to the tree
|
||||
else {
|
||||
tree.push(mappedElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tree.shift() || null;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export interface Message extends MessageEmojis {
|
||||
is_assistant: boolean;
|
||||
lang: string;
|
||||
created_date: string; // iso date string
|
||||
parent_id: string;
|
||||
parent_id: string | null;
|
||||
frontend_message_id?: string;
|
||||
user_id: string;
|
||||
user_is_author: boolean | null;
|
||||
@@ -40,3 +40,7 @@ export type FetchUserMessagesCursorResponse = {
|
||||
items: Message[];
|
||||
order: "asc" | "desc";
|
||||
};
|
||||
|
||||
export type MessageWithChildren = Message & {
|
||||
children: MessageWithChildren[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user