Skip to content

Commit

Permalink
Merge pull request #596 from cofacts/ai-reply-feedbacks
Browse files Browse the repository at this point in the history
Ai reply feedbacks
  • Loading branch information
MrOrz authored Jan 27, 2025
2 parents 3ca739b + 2f799dc commit dbec965
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 85 deletions.
6 changes: 5 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ PUBLIC_SLACK_IFTTT_APPLET_URL=

PUBLIC_LINE_IFTTT_TUTORIAL_YOUTUBEID=
PUBLIC_TELEGRAM_IFTTT_TUTORIAL_YOUTUBEID=
PUBLIC_SLACK_IFTTT_TUTORIAL_YOUTUBEID=
PUBLIC_SLACK_IFTTT_TUTORIAL_YOUTUBEID=

# Langfuse setup
PUBLIC_LANGFUSE_PUBLIC_KEY=
PUBLIC_LANGFUSE_HOST=
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -23,7 +28,7 @@ function AIReplySection({ defaultExpand = false, aiReplyText = '' }) {
}}
onClick={() => setExpand(v => !v)}
>
{t`Automated analysis from ChatGPT`}
{t`Automated analysis from AI`}
{expand ? <KeyboardArrowDownIcon /> : <KeyboardArrowUpIcon />}
</CardHeader>
{expand && (
Expand All @@ -34,6 +39,9 @@ function AIReplySection({ defaultExpand = false, aiReplyText = '' }) {
<div style={{ whiteSpace: 'pre-line', marginTop: 16 }}>
{aiReplyText}
</div>
<Box display="flex" justifyContent="space-between" mt={2}>
<VoteButtons aiResponseId={aiResponseId} />
</Box>
</CardContent>
)}
</Card>
Expand Down
220 changes: 220 additions & 0 deletions components/AIReplySection/VoteButtons.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null>(null);
const [currentVote, setCurrentVote] = useState<number>(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<HTMLElement>,
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 (
<>
<Box display="flex">
<Button
size="small"
variant="outlined"
type="button"
onClick={e => handleVoteClick(e, 1)}
className={cx(classes.vote, {
[classes.voted]: currentVote === 1,
})}
>
<ThumbUpIcon className={classes.thumbIcon} />
</Button>
<Button
size="small"
variant="outlined"
type="button"
onClick={e => handleVoteClick(e, -1)}
className={cx(classes.vote, {
[classes.voted]: currentVote === -1,
})}
>
<ThumbDownIcon className={classes.thumbIcon} />
</Button>
</Box>
<Popover
open={!!votePopoverAnchorEl}
anchorEl={votePopoverAnchorEl}
onClose={closeVotePopover}
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
classes={{ paper: classes.popover }}
>
<button
type="button"
className={classes.closeButton}
onClick={closeVotePopover}
>
<CloseIcon />
</button>
<Typography className={classes.popupTitle}>
{currentVote === 1
? t`Do you have anything to add?`
: t`Why do you think it is not useful?`}
</Typography>
<textarea
className={classes.textarea}
value={comment}
onChange={e => setComment(e.target.value)}
rows={10}
/>
<div className={classes.textCenter}>
<Button
className={classes.sendButton}
color="primary"
variant="contained"
disableElevation
disabled={!comment.trim()}
onClick={handleCommentSubmit}
>
{t`Send`}
</Button>
</div>
</Popover>
<Snackbar
open={showThankYouSnack}
onClose={() => setShowThankYouSnack(false)}
message={t`Thank you for the feedback.`}
/>
</>
);
}

export default VoteButtons;
2 changes: 2 additions & 0 deletions components/AIReplySection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import AIReplySection from './AIReplySection';
export default AIReplySection;
Loading

0 comments on commit dbec965

Please sign in to comment.