Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ai reply feedbacks #596

Merged
merged 26 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
85d5931
chore(package.json): install Langfuse SDK
MrOrz Jan 26, 2025
8b55537
feat(AIReplySection): add voting buttons for AI replies
MrOrz Jan 26, 2025
35d11c7
feat: Add vote handling placeholder for AI reply section
MrOrz Jan 26, 2025
aef3cdf
feat: Implement Langfuse vote tracking for AI replies
MrOrz Jan 26, 2025
3190e3c
style: Apply linter formatting to AIReplySection.js
MrOrz Jan 26, 2025
d91d8c4
feat(AIReplySection): pass aiResponseId to handleVote and update Grap…
MrOrz Jan 26, 2025
0e88460
feat(.env): add Langfuse configuration to .env.sample
MrOrz Jan 26, 2025
f87e748
feat(next.config.js): add Langfuse SDK support with babel-loader conf…
MrOrz Jan 26, 2025
fdbcc4c
feat(AIReplySection): integrate Langfuse SDK using next/config for co…
MrOrz Jan 26, 2025
8d0b42d
style(AIReplySection): adjust styling for vote button and thumb icon …
MrOrz Jan 26, 2025
cdf047c
refactor(AIReplySection): use Box prop instead
MrOrz Jan 26, 2025
b04f875
refactor: Extract vote buttons into separate reusable component
MrOrz Jan 26, 2025
4108f5d
style: Remove unused AIReplySection component
MrOrz Jan 26, 2025
f8217cb
feat(AIReplySection): add AIReplySection component with expandable vi…
MrOrz Jan 26, 2025
87b04eb
feat: Add comment popover to VoteButtons with Langfuse feedback
MrOrz Jan 26, 2025
9ad0d8a
style: Format code with linter and improve readability
MrOrz Jan 26, 2025
183ac1c
fix(components): remove unused file file
MrOrz Jan 26, 2025
3a8115b
feat(VoteButtons): add voted style to highlight selected votes
MrOrz Jan 26, 2025
b0f301a
feat: Implement vote buttons with immediate feedback and optional com…
MrOrz Jan 26, 2025
39b7c42
style: Apply linter formatting to VoteButtons component
MrOrz Jan 26, 2025
13a8cb4
feat: Remove duplicate className props in VoteButtons component
MrOrz Jan 26, 2025
cffe1de
feat(VoteButtons): improve vote handling and immediate feedback mecha…
MrOrz Jan 26, 2025
b417176
feat(VoteButtons): refactor vote ID handling and improve type safety
MrOrz Jan 26, 2025
c4a9297
chore(i18n): update translation
MrOrz Jan 27, 2025
12cd34f
feat(VoteButtons): disable send button when comment is empty
MrOrz Jan 27, 2025
2f799dc
feat: Add Snackbar for thank you message after feedback submission
MrOrz Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading