Files
Open-Assistant/website/src/components/FlaggableElement.tsx
T
rjmacarthy 2a8d38f058 Refactor fetch and post to use axios
Fix set_label id missing in payload use frontend_message_id

pre-commit

Refactor api fetcher/poster to axios create

lint

Remove string literal for path

Revert oasst_api_client.ts

Fix warning httpStatusCode OasstError optional parameter

Refactor remove api base url for local api

Lint add blank line
2023-01-13 17:05:10 +00:00

264 lines
6.9 KiB
TypeScript

import {
Button,
Checkbox,
Flex,
Grid,
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverTrigger,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Spacer,
Tooltip,
useBoolean,
useColorMode,
useColorModeValue,
useId,
} from "@chakra-ui/react";
import { FlagIcon, QuestionMarkCircleIcon } from "@heroicons/react/20/solid";
import { useEffect, useReducer } from "react";
import { get, post } from "src/lib/api";
import { Message } from "src/types/Conversation";
import { colors } from "styles/Theme/colors";
import useSWR from "swr";
import useSWRMutation from "swr/mutation";
interface Label {
name: string;
display_text: string;
help_text: string;
}
interface LoadLabelsAction {
type: "load_labels";
labels: Label[];
}
interface UpdateValueAction {
type: "update_value";
label_index: number;
value: number;
}
interface ToggleLabelAction {
type: "toggle_label";
label_index: number;
check: boolean;
}
interface LabelValue {
label: Label;
checked: boolean;
value: number;
}
interface FlagReportState {
label_values: LabelValue[];
submittable: boolean;
}
interface FlaggableElementProps {
children: React.ReactNode;
message: Message;
}
export const FlaggableElement = (props: FlaggableElementProps) => {
const [report, updateReport] = useReducer(
(state: FlagReportState, action: LoadLabelsAction | UpdateValueAction | ToggleLabelAction): FlagReportState => {
const makeState = (label_values: LabelValue[]): FlagReportState => {
const submittable = label_values.map(({ checked }) => checked).some(Boolean);
return { label_values, submittable };
};
switch (action.type) {
case "load_labels":
return makeState(
action.labels.map((label) => {
return { label, checked: false, value: 1 };
})
);
case "toggle_label": {
const values_copy = state.label_values.slice();
values_copy[action.label_index].checked = action.check;
return makeState(values_copy);
}
case "update_value": {
const values_copy = state.label_values.slice();
values_copy[action.label_index].value = action.value;
return makeState(values_copy);
}
}
},
{ label_values: [], submittable: false }
);
const [isEditing, setIsEditing] = useBoolean();
const backgroundColor = useColorModeValue("gray.200", "gray.700");
const { data, isLoading } = useSWR("/api/valid_labels", get);
useEffect(() => {
if (isLoading) {
return;
}
const { valid_labels } = data;
updateReport({ type: "load_labels", labels: valid_labels });
}, [data, isLoading]);
const { trigger } = useSWRMutation("/api/set_label", post, {
onSuccess: () => {
setIsEditing.off();
},
});
const submitResponse = () => {
const label_map: Map<string, number> = new Map();
report.label_values.forEach(({ label, checked, value }) => {
if (checked) {
label_map.set(label.name, value);
}
});
trigger({
message_id: props.message.id,
label_map: Object.fromEntries(label_map),
text: props.message.text,
});
};
const handleCheckboxState = (checked, label_index) => {
updateReport({ type: "toggle_label", label_index, check: checked });
};
const handleSliderState = (value, label_index) => {
updateReport({ type: "update_value", label_index, value });
};
return (
<Popover
isOpen={isEditing}
onOpen={setIsEditing.on}
onClose={setIsEditing.off}
closeOnBlur={false}
isLazy
lazyBehavior="keepMounted"
>
<Grid templateColumns="1fr min-content" gap={2}>
<PopoverAnchor>{props.children}</PopoverAnchor>
<Tooltip label="Report" bg="red.500">
<div>
<PopoverTrigger>
<Button h="full" bg={backgroundColor}>
<FlagIcon className="w-4 text-gray-400 group-hover:text-gray-500" aria-hidden="true" />
</Button>
</PopoverTrigger>
</div>
</Tooltip>
</Grid>
<PopoverContent width="fit-content">
<PopoverArrow />
<div className="relative h-4">
<PopoverCloseButton />
</div>
<PopoverBody>
{report.label_values.map(({ label, checked, value }, i) => (
<FlagCheckbox
label={label}
key={i}
idx={i}
checked={checked}
sliderValue={value}
checkboxHandler={handleCheckboxState}
sliderHandler={handleSliderState}
/>
))}
<Flex justify="center">
<Button
isDisabled={!report.submittable}
onClick={submitResponse}
className={`bg-indigo-600 text-${useColorModeValue(
colors.light.text,
colors.dark.text
)} hover:bg-indigo-700`}
>
Report
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
interface FlagCheckboxProps {
label: Label;
idx: number;
checked: boolean;
sliderValue: number;
checkboxHandler: (newVal: boolean, idx: number) => void;
sliderHandler: (newVal: number, idx: number) => void;
}
export function FlagCheckbox(props: FlagCheckboxProps): JSX.Element {
let AdditionalExplanation = null;
if (props.label.help_text) {
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>
);
}
const id = useId();
const { colorMode } = useColorMode();
const labelTextClass =
colorMode === "light"
? `text-${colors.light.text} hover:text-blue-700 float-left`
: `text-${colors.dark.text} hover:text-blue-400 float-left`;
return (
<Flex gap={1}>
<Checkbox
id={id}
isChecked={props.checked}
onChange={(e) => {
props.checkboxHandler(e.target.checked, props.idx);
}}
/>
<label className="text-sm form-check-label" htmlFor={id}>
<span className={labelTextClass}>{props.label.display_text}</span>
{AdditionalExplanation}
</label>
<Spacer />
<div
onClick={() => {
if (!props.checked) {
props.checkboxHandler(true, props.idx);
}
}}
>
<Slider
width="100px"
isDisabled={!props.checked}
defaultValue={100}
onChangeEnd={(val) => {
props.sliderHandler(val / 100, props.idx);
}}
>
<SliderTrack>
<SliderFilledTrack />
<SliderThumb />
</SliderTrack>
</Slider>
</div>
</Flex>
);
}