mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-06-27 16:10:30 +08:00
Add emoji reactions and reporting for messages (website) (#952)
* website: Move labelling to message ... menu and add reporting and emoji reactions We can add more emoji easily in future, we just need to pick ones that we have consistent icons for. Also added "open in new tab" option so that messages can be navigated to from tasks on mobile. * website: Make new label and report strings translatable. * website: Move report api call to oasst client * small fixes * pre-commit --------- Co-authored-by: AbdBarho <ka70911@gmail.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -29,18 +29,24 @@ Default.args = {
|
||||
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,
|
||||
@@ -50,12 +56,21 @@ Default.args = {
|
||||
export const Conversation = Template.bind({});
|
||||
Conversation.args = {
|
||||
messages: [
|
||||
{ text: "Hello! How can I help you?", is_assistant: true, id: "", frontend_message_id: "" },
|
||||
{
|
||||
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,
|
||||
@@ -70,18 +85,24 @@ LongText.args = {
|
||||
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,
|
||||
|
||||
@@ -11,11 +11,11 @@ interface MessageTableProps {
|
||||
export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) {
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
{messages.map((item, idx) => (
|
||||
{messages.map((message, idx) => (
|
||||
<MessageTableEntry
|
||||
enabled={enableLink}
|
||||
item={item}
|
||||
key={item.id + item.frontend_message_id}
|
||||
message={message}
|
||||
key={message.id + message.frontend_message_id}
|
||||
highlight={highlightLastMessage && idx === messages.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { Message } from "src/types/Conversation";
|
||||
|
||||
import { MessageTableEntry } from "./MessageTableEntry";
|
||||
|
||||
@@ -8,10 +9,8 @@ export default {
|
||||
component: MessageTableEntry,
|
||||
};
|
||||
|
||||
const Template = ({ text, is_assistant, id, frontend_message_id, enabled, highlight }) => {
|
||||
return (
|
||||
<MessageTableEntry item={{ text, is_assistant, id, frontend_message_id }} enabled={enabled} highlight={highlight} />
|
||||
);
|
||||
const Template = ({ enabled, highlight, ...message }) => {
|
||||
return <MessageTableEntry message={message as Message} enabled={enabled} highlight={highlight} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -22,6 +21,8 @@ Default.args = {
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
};
|
||||
|
||||
export const Asistant = Template.bind({});
|
||||
@@ -32,6 +33,8 @@ Asistant.args = {
|
||||
frontend_message_id: "",
|
||||
enabled: true,
|
||||
highlight: false,
|
||||
emojis: {},
|
||||
user_emojis: [],
|
||||
};
|
||||
|
||||
export const LongText = Template.bind({});
|
||||
@@ -42,4 +45,18 @@ LongText.args = {
|
||||
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,23 +1,47 @@
|
||||
import { Avatar, Box, HStack, 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 { 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 [emojis, 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");
|
||||
@@ -32,34 +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}
|
||||
outline={props.highlight && "2px solid black"}
|
||||
outlineColor={highlightColor}
|
||||
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(emojis.emojis).map(([emoji, count]) => (
|
||||
<MessageEmojiButton
|
||||
key={emoji}
|
||||
emoji={{ name: emoji, count }}
|
||||
checked={emojis.user_emojis.includes(emoji)}
|
||||
onClick={() => react(emoji, !emojis.user_emojis.includes(emoji))}
|
||||
/>
|
||||
))}
|
||||
<MessageActions
|
||||
react={react}
|
||||
userEmoji={emojis.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>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,6 @@ export const EvaluateTask = ({
|
||||
let messages = [];
|
||||
if (task.conversation) {
|
||||
messages = task.conversation.messages;
|
||||
messages = messages.map((message, index) => ({ ...message, id: index }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export const LabelTask = ({
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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, FetchUsersParams, FetchUsersResponse } from "src/types/Users";
|
||||
@@ -76,6 +76,27 @@ export class OasstApiClient {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message stats from the backend.
|
||||
*/
|
||||
@@ -154,6 +175,17 @@ export class OasstApiClient {
|
||||
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),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
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);
|
||||
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,9 +1,16 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
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}`, {
|
||||
const user = await getBackendUserCore(token.sub);
|
||||
const params = new URLSearchParams({
|
||||
username: user.id,
|
||||
auth_method: user.auth_method,
|
||||
});
|
||||
|
||||
const messageRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/messages/${id}/?${params}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-Key": process.env.FASTAPI_KEY,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { withoutRole } from "src/lib/auth";
|
||||
import { oasstApiClient } from "src/lib/oasst_api_client";
|
||||
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);
|
||||
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;
|
||||
@@ -6,7 +6,8 @@ import { withoutRole } from "src/lib/auth";
|
||||
*/
|
||||
const handler = withoutRole("banned", async (req, res, token) => {
|
||||
// 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 +18,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,
|
||||
|
||||
@@ -35,7 +35,7 @@ const MessageDetail = ({ id }: { id: string }) => {
|
||||
Parent
|
||||
</Text>
|
||||
<Box bg={backgroundColor} padding="4" borderRadius="xl" boxShadow="base" width="fit-content">
|
||||
<MessageTableEntry enabled item={parent} />
|
||||
<MessageTableEntry enabled message={parent} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
export interface Message {
|
||||
export type EmojiOp = "add" | "remove" | "toggle";
|
||||
|
||||
export interface MessageEmoji {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MessageEmojis {
|
||||
emojis: { [emoji: string]: number };
|
||||
user_emojis: string[];
|
||||
}
|
||||
|
||||
export interface Message extends MessageEmojis {
|
||||
text: string;
|
||||
is_assistant: boolean;
|
||||
id: string;
|
||||
created_date: string; // iso date string
|
||||
lang: string;
|
||||
frontend_message_id?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export const colors = {
|
||||
div: "white",
|
||||
text: "black",
|
||||
highlight: "blue.400",
|
||||
active: "blue.400",
|
||||
},
|
||||
dark: {
|
||||
bg: "gray.900",
|
||||
@@ -12,5 +13,6 @@ export const colors = {
|
||||
div: "gray.700",
|
||||
text: "gray.200",
|
||||
highlight: "blue.500",
|
||||
active: "blue.500",
|
||||
},
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -1,9 +1,8 @@
|
||||
import "i18next";
|
||||
|
||||
import type common from "public/locales/en/common.json";
|
||||
import type dashboard from "public/locales/en/dashboard.json";
|
||||
import type index from "public/locales/en/index.json";
|
||||
import type leaderboard from "public/locales/en/leaderboard.json";
|
||||
import type message from "public/locales/en/message.json";
|
||||
import type tasks from "public/locales/en/tasks.json";
|
||||
|
||||
declare module "i18next" {
|
||||
@@ -14,6 +13,7 @@ declare module "i18next" {
|
||||
index: typeof index;
|
||||
leaderboard: typeof leaderboard;
|
||||
tasks: typeof tasks;
|
||||
message: typeof message;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user