[eric] chat: add thumbs up/down feedback with a why-modal routed to cloud analytics

This commit is contained in:
ciregenz
2026-06-01 15:01:13 -07:00
parent c89ba4dc30
commit 06006cd54a
2 changed files with 144 additions and 0 deletions
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { report } from '@/shared/serviceClient';
export type Sentiment = 'up' | 'down';
interface Props {
open: boolean;
sentiment: Sentiment;
sessionId: string;
messageId: string;
onClose: () => void;
onSubmitted: () => void;
}
const FeedbackDialog: React.FC<Props> = ({ open, sentiment, sessionId, messageId, onClose, onSubmitted }) => {
const c = useClaudeTokens();
const [comment, setComment] = useState('');
const isUp = sentiment === 'up';
const handleSubmit = () => {
// Rides the same analytics channel as everything else (batches + offline
// spools to the cloud). Fire-and-forget, so the dialog closes instantly.
report('feedback', sentiment, { message_id: messageId, session_id: sessionId, comment: comment.trim() }, { immediate: true });
setComment('');
onSubmitted();
};
const handleClose = () => {
setComment('');
onClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
PaperProps={{ sx: { bgcolor: c.bg.elevated, borderRadius: 3, p: 1, minWidth: 420, maxWidth: 480 } }}
>
<Box sx={{ p: 2 }}>
<Typography sx={{ color: c.text.primary, fontSize: '1.15rem', fontWeight: 600, mb: 2 }}>
{isUp ? 'Give positive feedback' : 'Give negative feedback'}
</Typography>
<Typography sx={{ color: c.text.secondary, fontSize: '0.85rem', mb: 1 }}>
Please provide details: (optional)
</Typography>
<TextField
autoFocus
fullWidth
multiline
minRows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder={isUp ? 'What was good about this response?' : 'What went wrong with this response?'}
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: c.bg.page,
color: c.text.primary,
fontSize: '0.9rem',
'& fieldset': { borderColor: c.border.medium },
'&:hover fieldset': { borderColor: c.border.strong },
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
},
'& textarea::placeholder': { color: c.text.tertiary, opacity: 1 },
}}
/>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.75rem', fontStyle: 'italic', mt: 1.5 }}>
Shared with OpenSwarm to help improve your agents. We send your rating and note, not your conversation.
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 2 }}>
<Button onClick={handleClose} sx={{ color: c.text.secondary, textTransform: 'none' }}>
Cancel
</Button>
<Button
onClick={handleSubmit}
variant="contained"
sx={{
bgcolor: c.text.primary,
color: c.bg.page,
textTransform: 'none',
fontWeight: 600,
'&:hover': { bgcolor: c.text.secondary },
}}
>
Submit
</Button>
</Box>
</Box>
</Dialog>
);
};
export default FeedbackDialog;
@@ -11,7 +11,12 @@ import ReplayIcon from '@mui/icons-material/Replay';
import CallSplitIcon from '@mui/icons-material/CallSplit';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt';
import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt';
import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt';
import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import FeedbackDialog, { Sentiment } from './FeedbackDialog';
interface BranchNavProps {
currentIndex: number;
@@ -27,6 +32,8 @@ interface Props {
onRegenerate?: () => void;
onBranch?: () => void;
branchNav?: BranchNavProps;
sessionId?: string;
messageId?: string;
}
const btnSx = (c: ReturnType<typeof useClaudeTokens>) => ({
@@ -43,9 +50,13 @@ const MessageActionBar: React.FC<Props> = ({
onRegenerate,
onBranch,
branchNav,
sessionId,
messageId,
}) => {
const c = useClaudeTokens();
const [copied, setCopied] = useState(false);
const [dialogSentiment, setDialogSentiment] = useState<Sentiment | null>(null);
const [submitted, setSubmitted] = useState<Sentiment | null>(null);
const handleCopy = () => {
onCopy();
@@ -54,6 +65,7 @@ const MessageActionBar: React.FC<Props> = ({
};
const isUser = role === 'user';
const canRate = !isUser && !!sessionId && !!messageId;
return (
<Box
@@ -130,6 +142,24 @@ const MessageActionBar: React.FC<Props> = ({
{copied ? <CheckIcon sx={{ fontSize: 16 }} /> : <ContentCopyIcon sx={{ fontSize: 16 }} />}
</IconButton>
</Tooltip>
{canRate && (
<>
<Tooltip title="Give positive feedback" arrow>
<IconButton size="small" onClick={() => setDialogSentiment('up')} sx={btnSx(c)}>
{submitted === 'up'
? <ThumbUpAltIcon sx={{ fontSize: 16, color: c.accent.primary }} />
: <ThumbUpOffAltIcon sx={{ fontSize: 16 }} />}
</IconButton>
</Tooltip>
<Tooltip title="Give negative feedback" arrow>
<IconButton size="small" onClick={() => setDialogSentiment('down')} sx={btnSx(c)}>
{submitted === 'down'
? <ThumbDownAltIcon sx={{ fontSize: 16, color: c.accent.primary }} />
: <ThumbDownOffAltIcon sx={{ fontSize: 16 }} />}
</IconButton>
</Tooltip>
</>
)}
{onRegenerate && (
<Tooltip title="Regenerate" arrow>
<IconButton size="small" onClick={onRegenerate} sx={btnSx(c)}>
@@ -146,6 +176,16 @@ const MessageActionBar: React.FC<Props> = ({
)}
</>
)}
{canRate && dialogSentiment && (
<FeedbackDialog
open
sentiment={dialogSentiment}
sessionId={sessionId!}
messageId={messageId!}
onClose={() => setDialogSentiment(null)}
onSubmitted={() => { setSubmitted(dialogSentiment); setDialogSentiment(null); }}
/>
)}
</Box>
);
};
@@ -159,4 +199,6 @@ export default React.memo(MessageActionBar, (prev, next) => (
&& !!prev.onBranch === !!next.onBranch
&& prev.branchNav?.currentIndex === next.branchNav?.currentIndex
&& prev.branchNav?.totalBranches === next.branchNav?.totalBranches
&& prev.messageId === next.messageId
&& prev.sessionId === next.sessionId
));