Merge pull request #189 from BitterKanegul/42-implement-text-labels-web-frontend

#42 Adding popover to flag text with labels
This commit is contained in:
Keith Stevens
2023-01-02 14:25:11 +09:00
committed by GitHub
4 changed files with 230 additions and 6 deletions
+216
View File
@@ -0,0 +1,216 @@
import {
Button,
Checkbox,
Flex,
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverTrigger,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Spacer,
useBoolean,
} from "@chakra-ui/react";
import { FlagIcon, QuestionMarkCircleIcon } from "@heroicons/react/20/solid";
import { useState } from "react";
import poster from "src/lib/poster";
import useSWRMutation from "swr/mutation";
export const FlaggableElement = (props) => {
const [isEditing, setIsEditing] = useBoolean();
const { trigger } = useSWRMutation("/api/v1/text_labels", poster, {
onSuccess: () => {
setIsEditing.off;
},
});
const submitResponse = () => {
const label_map: Map<string, number> = new Map();
TEXT_LABEL_FLAGS.forEach((flag, i) => {
if (checkboxValues[i]) {
label_map.set(flag.attributeName, sliderValues[i]);
}
});
trigger({ post_id: props.post_id, label_map: Object.fromEntries(label_map), text: props.text });
};
const [checkboxValues, setCheckboxValues] = useState(new Array(TEXT_LABEL_FLAGS.length).fill(false));
const [sliderValues, setSliderValues] = useState(new Array(TEXT_LABEL_FLAGS.length).fill(1));
const handleCheckboxState = (isChecked, idx) => {
setCheckboxValues(
checkboxValues.map((val, i) => {
return i == idx ? isChecked : val;
})
);
};
const handleSliderState = (newVal, idx) => {
setSliderValues(
sliderValues.map((val, i) => {
return i == idx ? newVal : val;
})
);
};
return (
<Popover
isOpen={isEditing}
onOpen={setIsEditing.on}
onClose={setIsEditing.off}
closeOnBlur={false}
isLazy
lazyBehavior="keepMounted"
>
<div className="inline-block float-left">
<PopoverAnchor>{props.children}</PopoverAnchor>
<PopoverTrigger>
<Button color="transparent">
<FlagIcon
className="h-5 w-5 ml-3 align-center text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Button>
</PopoverTrigger>
</div>
<PopoverContent width="fit-content">
<PopoverArrow />
<PopoverCloseButton />
<div className="flex mt-3 ">
<PopoverBody>
<ul>
{TEXT_LABEL_FLAGS.map((option, i) => {
return (
<FlagCheckboxLi
option={option}
key={i}
idx={i}
checkboxValues={checkboxValues}
sliderValues={sliderValues}
checkboxHandler={handleCheckboxState}
sliderHandler={handleSliderState}
></FlagCheckboxLi>
);
})}
</ul>
<div className="flex justify-center ml-auto">
<Button
isDisabled={
!checkboxValues.reduce((all, current) => {
return all | current;
}, false)
}
onClick={() => submitResponse()}
className="bg-indigo-600 text-black hover:bg-indigo-700"
>
Report
</Button>
</div>
</PopoverBody>
</div>
</PopoverContent>
</Popover>
);
};
function FlagCheckboxLi(props: {
option: textFlagLabels;
idx: number;
checkboxValues: boolean[];
sliderValues: number[];
checkboxHandler: (newVal: boolean, idx: number) => void;
sliderHandler: (newVal: number, idx: number) => void;
}): JSX.Element {
let AdditionalExplanation = null;
if (props.option.additionalExplanation) {
AdditionalExplanation = (
<a href="#" className="group flex items-center space-x-2.5 text-sm ">
<QuestionMarkCircleIcon
className="flex h-5 w-5 ml-3 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</a>
);
}
return (
<li>
<Flex>
<Checkbox
onChange={(e) => {
props.checkboxHandler(e.target.checked, props.idx);
}}
/>
<label
className=" ml-1 mr-1 text-sm form-check-label hover:cursor-pointer"
htmlFor={props.option.attributeName}
>
<span className="text-gray-800 hover:text-blue-700 float-left">{props.option.labelText}</span>
{AdditionalExplanation}
</label>
<Spacer />
<Slider
width="100px"
isDisabled={!props.checkboxValues[props.idx]}
defaultValue={100}
onChangeEnd={(val) => {
props.sliderHandler(val / 100, props.idx);
}}
>
<SliderTrack>
<SliderFilledTrack />
<SliderThumb />
</SliderTrack>
</Slider>
</Flex>
</li>
);
}
interface textFlagLabels {
attributeName: string;
labelText: string;
additionalExplanation?: string;
}
const TEXT_LABEL_FLAGS: textFlagLabels[] = [
// For the time being this list is configured on the FE.
// In the future it may be provided by the API.
{
attributeName: "fails_task",
labelText: "Fails to follow the correct instruction / task",
additionalExplanation: "__TODO__",
},
{
attributeName: "not_customer_assistant_appropriate",
labelText: "Inappropriate for customer assistant",
additionalExplanation: "__TODO__",
},
{
attributeName: "contains_sexual_content",
labelText: "Contains sexual content",
},
{
attributeName: "contains_violent_content",
labelText: "Contains violent content",
},
{
attributeName: "encourages_violence",
labelText: "Encourages or fails to discourage violence/abuse/terrorism/self-harm",
},
{
attributeName: "denigrates_a_protected_class",
labelText: "Denigrates a protected class",
},
{
attributeName: "gives_harmful_advice",
labelText: "Fails to follow the correct instruction / task",
additionalExplanation:
"The advice given in the output is harmful or counter-productive. This may be in addition to, but is distinct from the question about encouraging violence/abuse/terrorism/self-harm.",
},
{
attributeName: "expresses_moral_judgement",
labelText: "Expresses moral judgement",
},
];
+12 -3
View File
@@ -1,3 +1,5 @@
import { FlaggableElement } from "./FlaggableElement";
export interface Message {
text: string;
is_assistant: boolean;
@@ -5,11 +7,18 @@ export interface Message {
const getColor = (isAssistant: boolean) => (isAssistant ? "bg-slate-800" : "bg-sky-900");
export const Messages = ({ messages }: { messages: Message[] }) => {
export const Messages = ({ messages, post_id }: { messages: Message[]; post_id: string }) => {
const items = messages.map(({ text, is_assistant }: Message, i: number) => {
return (
<div key={i + text} className={`${getColor(is_assistant)} p-4 my-1 rounded-xl text-white whitespace-pre-wrap`}>
{text}
<div className="flex" key={i + text}>
<FlaggableElement text={text} post_id={post_id}>
<div
key={i + text}
className={`${getColor(is_assistant)} p-4 my-1 rounded-xl text-white whitespace-pre-wrap float-left mr-3`}
>
{text}
</div>
</FlaggableElement>
</div>
);
});
+1 -1
View File
@@ -61,7 +61,7 @@ const AssistantReply = () => {
<>
<h5 className="text-lg font-semibold">Reply as the assistant</h5>
<p className="text-lg py-1">Given the following conversation, provide an adequate reply</p>
<Messages messages={task.conversation.messages} />
<Messages messages={task.conversation.messages} post_id={task.id} />
</>
<Textarea name="reply" placeholder="Reply..." ref={inputRef} />
</TwoColumns>
+1 -2
View File
@@ -61,7 +61,7 @@ const UserReply = () => {
<>
<h5 className="text-lg font-semibold">Reply as a user</h5>
<p className="text-lg py-1">Given the following conversation, provide an adequate reply</p>
<Messages messages={task.conversation.messages} />
<Messages messages={task.conversation.messages} post_id={task.id} />
{task.hint && <p className="text-lg py-1">Hint: {task.hint}</p>}
</>
<Textarea name="reply" placeholder="Reply..." ref={inputRef} />
@@ -69,7 +69,6 @@ const UserReply = () => {
<section className="mb-8 p-4 rounded-lg shadow-lg bg-white flex flex-row justify-items-stretch ">
<TaskInfo id={tasks[0].id} output="Submit your answer" />
<Flex justify="center" ml="auto" gap={2}>
<SkipButton>Skip</SkipButton>
{endTask.task.type !== "task_done" ? (