implement admin message detail page
diff --git a/website/src/components/Messages/MessageTableEntry.tsx b/website/src/components/Messages/MessageTableEntry.tsx
index d968678689..b8ff9ef98a 100644
--- a/website/src/components/Messages/MessageTableEntry.tsx
+++ b/website/src/components/Messages/MessageTableEntry.tsx
@@ -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>
diff --git a/website/src/components/Messages/MessageTree.tsx b/website/src/components/Messages/MessageTree.tsx
new file mode 100644
index 0000000000..639e133067
--- /dev/null
+++ b/website/src/components/Messages/MessageTree.tsx
@@ -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>
+  );
+};
diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts
index a36073232c..aec9b2e8df 100644
--- a/website/src/lib/oasst_api_client.ts
+++ b/website/src/lib/oasst_api_client.ts
@@ -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
    */
diff --git a/website/src/pages/admin/messages/[id].tsx b/website/src/pages/admin/messages/[id].tsx
new file mode 100644
index 0000000000..8c725d07a9
--- /dev/null
+++ b/website/src/pages/admin/messages/[id].tsx
@@ -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"])),
+    },
+  };
+};
diff --git a/website/src/pages/api/admin/messages/[id]/tree.ts b/website/src/pages/api/admin/messages/[id]/tree.ts
new file mode 100644
index 0000000000..0688dab8e1
--- /dev/null
+++ b/website/src/pages/api/admin/messages/[id]/tree.ts
@@ -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;
+};
diff --git a/website/src/types/Conversation.ts b/website/src/types/Conversation.ts
index 57a9efbb4d..f5841f2722 100644
--- a/website/src/types/Conversation.ts
+++ b/website/src/types/Conversation.ts
@@ -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[];
+};

- Add admin message detail page.
- Add new message tree UI and use/tree endpoint to receive data.
- New message tree only being used in the admin panel. Not in the user area yet.
- The current tree UI display is not really well in mobile when the depth > 4. I will address this in the next PR.

Message tree UI
![image](https://user-images.githubusercontent.com/33456881/218231870-bcb02f5c-7557-4a35-b0bb-d4352e02fd53.png)
