diff --git a/.env.sample b/.env.sample index b719054a..7463eab0 100644 --- a/.env.sample +++ b/.env.sample @@ -50,4 +50,8 @@ PUBLIC_SLACK_IFTTT_APPLET_URL= PUBLIC_LINE_IFTTT_TUTORIAL_YOUTUBEID= PUBLIC_TELEGRAM_IFTTT_TUTORIAL_YOUTUBEID= -PUBLIC_SLACK_IFTTT_TUTORIAL_YOUTUBEID= \ No newline at end of file +PUBLIC_SLACK_IFTTT_TUTORIAL_YOUTUBEID= + +# Langfuse setup +PUBLIC_LANGFUSE_PUBLIC_KEY= +PUBLIC_LANGFUSE_HOST= diff --git a/components/AIReplySection.js b/components/AIReplySection/AIReplySection.js similarity index 78% rename from components/AIReplySection.js rename to components/AIReplySection/AIReplySection.js index a851e027..7fb7fdab 100644 --- a/components/AIReplySection.js +++ b/components/AIReplySection/AIReplySection.js @@ -1,13 +1,18 @@ import { useState } from 'react'; import { t } from 'ttag'; - +import { Box } from '@material-ui/core'; import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; import { Card, CardHeader, CardContent } from 'components/Card'; import Hint from 'components/NewReplySection/ReplyForm/Hint'; +import VoteButtons from './VoteButtons'; -function AIReplySection({ defaultExpand = false, aiReplyText = '' }) { +function AIReplySection({ + defaultExpand = false, + aiReplyText = '', + aiResponseId, +}) { const [expand, setExpand] = useState(defaultExpand); return ( @@ -23,7 +28,7 @@ function AIReplySection({ defaultExpand = false, aiReplyText = '' }) { }} onClick={() => setExpand(v => !v)} > - {t`Automated analysis from ChatGPT`} + {t`Automated analysis from AI`} {expand ? : } {expand && ( @@ -34,6 +39,9 @@ function AIReplySection({ defaultExpand = false, aiReplyText = '' }) {
{aiReplyText}
+ + + )} diff --git a/components/AIReplySection/VoteButtons.tsx b/components/AIReplySection/VoteButtons.tsx new file mode 100644 index 00000000..1b59d24d --- /dev/null +++ b/components/AIReplySection/VoteButtons.tsx @@ -0,0 +1,220 @@ +import { + Button, + Box, + makeStyles, + Popover, + Typography, + Snackbar, +} from '@material-ui/core'; +import cx from 'classnames'; +import CloseIcon from '@material-ui/icons/Close'; +import { t } from 'ttag'; +import { useState } from 'react'; +import { LangfuseWeb } from 'langfuse'; +import getConfig from 'next/config'; +import { ThumbUpIcon, ThumbDownIcon } from 'components/icons'; + +const { + publicRuntimeConfig: { PUBLIC_LANGFUSE_PUBLIC_KEY, PUBLIC_LANGFUSE_HOST }, +} = getConfig(); + +const langfuseWeb = new LangfuseWeb({ + publicKey: PUBLIC_LANGFUSE_PUBLIC_KEY, + baseUrl: PUBLIC_LANGFUSE_HOST, +}); + +const useStyles = makeStyles(theme => ({ + vote: { + borderRadius: 45, + marginRight: 3, + [theme.breakpoints.up('md')]: { + marginRight: 10, + }, + }, + voted: { + color: `${theme.palette.primary[500]} !important`, + }, + thumbIcon: { + fontSize: 20, + fill: 'transparent', + stroke: 'currentColor', + }, + popover: { + position: 'relative', + width: 420, + maxWidth: '90vw', + padding: 32, + }, + closeButton: { + background: theme.palette.common.white, + cursor: 'pointer', + position: 'absolute', + right: 6, + top: 10, + border: 'none', + outline: 'none', + color: theme.palette.secondary[100], + }, + popupTitle: { + fontSize: 18, + marginBottom: 24, + }, + textarea: { + padding: 15, + width: '100%', + borderRadius: 8, + border: `1px solid ${theme.palette.secondary[100]}`, + outline: 'none', + '&:focus': { + border: `1px solid ${theme.palette.primary[500]}`, + }, + }, + textCenter: { textAlign: 'center' }, + sendButton: { + marginTop: 10, + borderRadius: 30, + }, +})); + +type Props = { + aiResponseId: string; +}; + +// One browser refresh represents one voter +const aiReplyVoterId = Math.random() + .toString(36) + .substring(2); + +function VoteButtons({ aiResponseId }: Props) { + const classes = useStyles(); + const [ + votePopoverAnchorEl, + setVotePopoverAnchorEl, + ] = useState(null); + const [currentVote, setCurrentVote] = useState(0); + const [comment, setComment] = useState(''); + const [showThankYouSnack, setShowThankYouSnack] = useState(false); + + // Creates and updates score using the same ID + const scoreId = `${aiResponseId}__${aiReplyVoterId}`; + + const handleVoteClick = async ( + event: React.MouseEvent, + vote: number + ) => { + const buttonElem = event.target as HTMLElement; + // If clicking same vote again, set to 0 (no vote) + const newVote = vote === currentVote ? 0 : vote; + + // Send vote immediately, no ned to wait + langfuseWeb.score({ + id: scoreId, + traceId: aiResponseId, + name: 'user-feedback', + value: newVote, + }); + + setCurrentVote(newVote); + + // Only open popover if setting a new vote (not removing) + if (newVote !== 0) { + setVotePopoverAnchorEl(buttonElem); + } + }; + + const closeVotePopover = () => { + setVotePopoverAnchorEl(null); + setComment(''); + }; + + const handleCommentSubmit = async () => { + if (currentVote === 0 || !comment.trim()) return; + + await langfuseWeb.score({ + id: scoreId, + traceId: aiResponseId, + name: 'user-feedback', + value: currentVote, + comment, + }); + closeVotePopover(); + setShowThankYouSnack(true); + }; + + return ( + <> + + + + + + + + {currentVote === 1 + ? t`Do you have anything to add?` + : t`Why do you think it is not useful?`} + +